Skip to main content
Advertisement

18.2 Environment Setup

There are two main ways to start a Solid.js project: copy the official template using degit, or add the Solid plugin to Vite. This chapter covers both approaches, along with TypeScript configuration, developer tooling, and ESLint/Prettier to build a complete development environment.


Creating a Project

# JavaScript template
npx degit solidjs/templates/js my-solid-app
cd my-solid-app
npm install
npm run dev
# TypeScript template (recommended)
npx degit solidjs/templates/ts my-solid-app
cd my-solid-app
npm install
npm run dev

degit is a tool that copies a Git repository without its history. It is faster than git clone and keeps your starting point clean.

Method 2: Vite + Solid Plugin (Manual Setup)

# Create a Vite project (interactive)
npm create vite@latest my-solid-app -- --template solid
cd my-solid-app
npm install
npm run dev

# Or the TypeScript version
npm create vite@latest my-solid-app -- --template solid-ts

Method 3: SolidStart (Full-stack Framework)

npm create solid@latest
# Follow the interactive installer
# ✔ Which template would you like to use? › bare / hackernews / with-auth / ...
# ✔ Use TypeScript? › Yes
# ✔ Install dependencies? › Yes

SolidStart is the full-stack meta-framework for Solid.js — think of it as the Next.js equivalent for Solid. It provides SSR, SSG, and file-based routing.


Project Structure

TypeScript project structure based on solidjs/templates/ts:

my-solid-app/
├── src/
│ ├── App.tsx # Root component
│ ├── App.module.css # CSS Module for App
│ ├── index.tsx # Entry point (calls render)
│ ├── logo.svg # Example asset
│ └── components/ # (create manually) components folder
│ ├── Header.tsx
│ ├── Counter.tsx
│ └── ...
├── public/
│ └── favicon.ico # Static assets
├── index.html # HTML entry point (Vite)
├── vite.config.ts # Vite configuration
├── tsconfig.json # TypeScript configuration
└── package.json

src/index.tsx — Entry Point

/* @refresh reload */
import { render } from 'solid-js/web';
import './index.css';
import App from './App';

const root = document.getElementById('root');

if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(
'Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?',
);
}

render(() => <App />, root!);

The /* @refresh reload */ comment tells Solid's Babel plugin that when this file changes during HMR (Hot Module Replacement), the entire page should be reloaded.

src/App.tsx — Root Component

import type { Component } from 'solid-js';
import { createSignal } from 'solid-js';
import styles from './App.module.css';
import logo from './logo.svg';

const App: Component = () => {
const [count, setCount] = createSignal(0);

return (
<div class={styles.App}>
<header class={styles.header}>
<img src={logo} class={styles.logo} alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<p>
Count: {count()}
<button onClick={() => setCount(count() + 1)}>+1</button>
</p>
</header>
</div>
);
};

export default App;

Vite Configuration (vite-plugin-solid)

The vite.config.ts file is the heart of a Solid.js project:

// vite.config.ts
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';

export default defineConfig({
plugins: [
solidPlugin({
// Solid Babel transform options
solid: {
// Preserve component names in development (for debugging)
generate: 'dom',
},
}),
],

// Dev server options
server: {
port: 3000,
open: true, // Automatically open browser
},

// Build options
build: {
target: 'esnext',
outDir: 'dist',
sourcemap: true, // Generate source maps
},

// Path aliases
resolve: {
alias: {
'@': '/src', // import '@/components/Button'
'~': '/src',
},
},
});

TypeScript Configuration for Path Aliases

Aliases must also be added to tsconfig.json for IDE autocomplete to work:

{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"strict": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"~/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

TypeScript Template Details

Solid.js-Specific TypeScript Types

// Example usage of solid-js types
import type { Component, JSX, ParentComponent, FlowComponent } from 'solid-js';

// Basic component
const Button: Component<{ onClick: () => void; label: string }> = (props) => (
<button onClick={props.onClick}>{props.label}</button>
);

// Component that accepts children
const Card: ParentComponent<{ title: string }> = (props) => (
<div class="card">
<h2>{props.title}</h2>
{props.children}
</div>
);

// Render prop pattern
const List: FlowComponent<{ items: string[] }, JSX.Element> = (props) => (
<ul>
{props.items.map(item => <li>{props.children(item)}</li>)}
</ul>
);

CSS Module Type Setup

Vite supports CSS Modules out of the box:

// Type declarations for *.module.css files if needed
// src/vite-env.d.ts (auto-generated)
/// <reference types="vite/client" />

Running the Dev Server and HMR

# Start the dev server (default port: 3000)
npm run dev

# Run on a specific port
npm run dev -- --port 4000

# Expose to network (accessible from other devices)
npm run dev -- --host

How HMR (Hot Module Replacement) Works

HMR in Solid.js is handled by vite-plugin-solid. When a component file is modified:

  1. Only the changed file is recompiled
  2. The update is sent to the browser via WebSocket
  3. Only the component is replaced, preserving state
# Production build
npm run build

# Preview the build output locally
npm run preview

Build Output Structure

dist/
├── assets/
│ ├── index-[hash].js # Bundled JavaScript
│ └── index-[hash].css # Bundled CSS
└── index.html # HTML entry point

Installing and Using Solid DevTools

Installing the Browser Extension

Chrome/Edge: Search for Solid DevTools on the Chrome Web Store

Or install directly:

# Install as a dev dependency
npm install --save-dev solid-devtools

Add the plugin to vite.config.ts:

import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
import devtools from 'solid-devtools/vite'; // Add this

export default defineConfig({
plugins: [
devtools({
autoname: true, // Automatically infer component names
locator: {
targetIDE: 'vscode', // Open file in VS Code when clicked
componentLocation: true,
jsxLocation: true,
},
}),
solidPlugin(),
],
});

Add the import to src/index.tsx:

import 'solid-devtools'; // Add at the top
/* @refresh reload */
import { render } from 'solid-js/web';
// ...

Key DevTools Features

TabFeature
ComponentComponent tree, live props/signals inspection
SignalList of all Signals in the app with current values
Owner GraphDependency graph of Signals and Effects
LocatorClick a DOM element to jump directly to its source file

ESLint + Prettier Setup

Package Installation

# ESLint + Solid plugin
npm install --save-dev \
eslint \
@typescript-eslint/parser \
@typescript-eslint/eslint-plugin \
eslint-plugin-solid \
eslint-config-prettier \
prettier

ESLint Configuration (eslint.config.js, Flat Config style)

// eslint.config.js
import js from '@eslint/js';
import ts from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import solid from 'eslint-plugin-solid';
import prettier from 'eslint-config-prettier';

export default [
js.configs.recommended,

// TypeScript
{
files: ['**/*.{ts,tsx}'],
plugins: {
'@typescript-eslint': ts,
},
languageOptions: {
parser: tsParser,
parserOptions: {
project: './tsconfig.json',
},
},
rules: {
...ts.configs.recommended.rules,
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
},
},

// Solid.js-specific rules
{
files: ['**/*.{jsx,tsx}'],
plugins: { solid },
rules: {
...solid.configs.typescript.rules,
// Core Solid.js rules
'solid/no-destructure': 'error', // Prohibit destructuring props
'solid/reactivity': 'warn', // Warn on reactivity pattern issues
'solid/event-handlers': 'error', // Event handler naming convention
},
},

// Disable rules that conflict with Prettier (always last)
prettier,

// Excluded files
{
ignores: ['dist/**', 'node_modules/**'],
},
];

Prettier Configuration (.prettierrc)

{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"jsxSingleQuote": false,
"bracketSameLine": false
}

VS Code Settings (.vscode/settings.json)

{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"typescript.preferences.importModuleSpecifier": "shortest"
}

package.json Scripts

{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"format": "prettier --write src/**/*.{ts,tsx,css}",
"type-check": "tsc --noEmit"
}
}

// .vscode/extensions.json
{
"recommendations": [
"esbenp.prettier-vscode", // Prettier formatter
"dbaeumer.vscode-eslint", // ESLint integration
"bradlc.vscode-tailwindcss", // TailwindCSS (if used)
"ms-vscode.vscode-typescript-next", // Latest TypeScript
"YoavBls.pretty-ts-errors", // Improved TS error readability
"solid.solid-snippets" // Solid.js snippets (if available)
]
}

Practical Example: Complete Project Initial Structure

Recommended directory structure for real-world projects:

src/
├── components/ # Reusable UI components
│ ├── ui/ # Base UI (Button, Input, Card, etc.)
│ │ ├── Button.tsx
│ │ ├── Input.tsx
│ │ └── index.ts # Barrel export
│ └── features/ # Feature-specific components
│ ├── auth/
│ └── dashboard/
├── stores/ # Global state (createStore)
│ ├── userStore.ts
│ └── appStore.ts
├── hooks/ # Custom Signal/Effect hooks
│ ├── useLocalStorage.ts
│ └── useFetch.ts
├── pages/ # Route-level page components
│ ├── Home.tsx
│ └── About.tsx
├── lib/ # Utility functions, API client
│ ├── api.ts
│ └── utils.ts
├── styles/ # Global CSS
│ ├── global.css
│ └── variables.css
├── App.tsx
└── index.tsx

src/lib/utils.ts — Common Utilities

// Type-safe class name composition utility
export function cx(...classes: (string | undefined | false | null)[]): string {
return classes.filter(Boolean).join(' ');
}

// Debounce function
export function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}

src/components/ui/Button.tsx — Reusable Button

import type { Component, JSX } from 'solid-js';
import { splitProps } from 'solid-js';

type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
type ButtonSize = 'sm' | 'md' | 'lg';

interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
loading?: boolean;
}

const variantStyles: Record<ButtonVariant, string> = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700',
ghost: 'bg-transparent text-gray-600 hover:bg-gray-100',
};

const sizeStyles: Record<ButtonSize, string> = {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
};

const Button: Component<ButtonProps> = (props) => {
// Use splitProps to separate component-specific props from native HTML props
const [local, rest] = splitProps(props, ['variant', 'size', 'loading', 'class', 'children']);

const variant = () => local.variant ?? 'primary';
const size = () => local.size ?? 'md';

return (
<button
{...rest}
disabled={local.loading || rest.disabled}
class={`
inline-flex items-center justify-center rounded font-medium
transition-colors focus:outline-none focus:ring-2
disabled:opacity-50 disabled:cursor-not-allowed
${variantStyles[variant()]}
${sizeStyles[size()]}
${local.class ?? ''}
`.trim()}
>
{local.loading ? 'Loading...' : local.children}
</button>
);
};

export default Button;

Pro Tips

Tip 1: What /* @refresh reload */ Means

This comment instructs HMR to reload the entire page when component state cannot be preserved. It should only go in the entry point (index.tsx) — regular components do not need it.

Tip 2: Type Environment Variables for Vite

// src/env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
// Declare all environment variables you use
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}
# .env.local (included in .gitignore)
VITE_API_URL=http://localhost:8080
VITE_APP_TITLE=My Solid App
// Usage
const API_URL = import.meta.env.VITE_API_URL;

Tip 3: Analyze Bundle Size

# Visualize the bundle with rollup-plugin-visualizer
npm install --save-dev rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
plugins: [
solidPlugin(),
visualizer({
open: true, // Automatically open browser after build
filename: 'dist/stats.html',
gzipSize: true,
}),
],
});
npm run build  # After building, dist/stats.html opens in the browser

Tip 4: Improve Maintainability with Absolute Imports

// Relative path (bad)
import Button from '../../../components/ui/Button';

// Absolute path (good)
import Button from '@/components/ui/Button';

Both resolve.alias in vite.config.ts and paths in tsconfig.json must be configured.

Tip 5: Using Solid.js with Tailwind CSS

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
export default {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}', // Include Solid files
],
theme: {
extend: {},
},
plugins: [],
};
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Summary

ItemCommand / File
Create JS templatenpx degit solidjs/templates/js my-app
Create TS templatenpx degit solidjs/templates/ts my-app
Vite configurationvite.config.ts + vite-plugin-solid
Dev servernpm run dev (default port 3000)
HMRControlled via /* @refresh reload */ comment
DevToolssolid-devtools package + browser extension
ESLinteslint-plugin-solid (no-destructure rule is essential)
Path aliasesvite.config.ts alias + tsconfig.json paths

In the next chapter, we will take a deep dive into Solid.js's core reactive primitives: Signal, Effect, Memo, and Resource.

Advertisement