diff --git a/examples/_data.ts b/examples/_data.ts index 33a8270dc..af28a1358 100644 --- a/examples/_data.ts +++ b/examples/_data.ts @@ -264,6 +264,11 @@ export const sidebar = [ externalURL: "https://www.youtube.com/watch?v=MDPauM8fZDE", type: "video", }, + { + label: "Build a SolidJS app", + id: "/examples/solidjs_tutorial", + type: "tutorial", + }, { label: "Build a React app", id: "/examples/react_app_video", diff --git a/examples/tutorials/images/how-to/solidjs/demo.mp4 b/examples/tutorials/images/how-to/solidjs/demo.mp4 new file mode 100644 index 000000000..59578baa0 Binary files /dev/null and b/examples/tutorials/images/how-to/solidjs/demo.mp4 differ diff --git a/examples/tutorials/solidjs.md b/examples/tutorials/solidjs.md new file mode 100644 index 000000000..27acc1c26 --- /dev/null +++ b/examples/tutorials/solidjs.md @@ -0,0 +1,398 @@ +--- +title: "Build a SolidJS app with Deno" +url: /examples/solidjs_tutorial/ +--- + +[SolidJS](https://www.solidjs.com/) is a declarative JavaScript library for +creating user interfaces that emphasizes fine-grained reactivity and minimal +overhead. When combined with Deno's modern runtime environment, you get a +powerful, performant stack for building web applications. In this tutorial, +we'll build a simple dinosaur catalog app that demonstrates the key features of +both technologies. + +We'll go over how to build a simple SolidJS app using Deno: + +- [Scaffold a SolidJS app](#scaffold-a-solidjs-app-with-vite) +- [Set up on Hono backend](#set-up-our-hono-backend) +- [Create our SolidJS frontend](#create-our-solidjs-frontend) +- [Next steps](#next-steps) + +Feel free to skip directly to +[the source code](https://github.com/denoland/examples/tree/main/with-solidjs) +or follow along below! + +## Scaffold a SolidJS app with Vite + +Let's set up our SolidJS application using [Vite](https://vite.dev/), a modern +build tool that provides an excellent development experience with features like +hot module replacement and optimized builds. + +```bash +deno init --npm vite@latest solid-deno --template solid-ts +``` + +Our backend will be powered by [Hono](https://hono.dev/), which we can install +via [JSR](https://jsr.io). Let's also add `solidjs/router` for client-side +routing and navigation between our dinosaur catalog pages. + +
+ +```bash +deno add jsr:@hono/hono npm:@solidjs/router +``` + +
+ +Learn more about deno add and using Deno as a package manager. + +
+
+ +We'll also have to update our `deno.json` to include a few tasks and +`compilerOptions` to run our app: + +
+ +```json +{ + "tasks": { + "dev": "deno task dev:api & deno task dev:vite", + "dev:api": "deno run --allow-env --allow-net --allow-read api/main.ts", + "dev:vite": "deno run -A npm:vite", + "build": "deno run -A npm:vite build", + "serve": { + "command": "deno task dev:api", + "description": "Run the build, and then start the API server", + "dependencies": ["deno task build"] + } + }, + "imports": { + "@hono/hono": "jsr:@hono/hono@^4.6.12", + "@solidjs/router": "npm:@solidjs/router@^0.14.10" + }, + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "solid-js", + "lib": ["DOM", "DOM.Iterable", "ESNext"] + } +} +``` + +
+You can write your tasks as objects. Here our serve command includes a description and dependencies. +
+
+ +Great! Next, let's setup our API backend. + +## Set up our Hono backend + +Within our main directory, we will set up an `api/` directory and create two +files. First, our dinosaur data file, +[`api/data.json`](https://github.com/denoland/examples/blob/main/with-solidjs/api/data.json): + +```json +// api/data.json + +[ + { + "name": "Aardonyx", + "description": "An early stage in the evolution of sauropods." + }, + { + "name": "Abelisaurus", + "description": "\"Abel's lizard\" has been reconstructed from a single skull." + }, + { + "name": "Abrictosaurus", + "description": "An early relative of Heterodontosaurus." + }, + ... +] +``` + +This is where our data will be pulled from. In a full application, this data +would come from a database. + +> ⚠️️ In this tutorial we hard code the data. But you can connect +> to [a variety of databases](https://docs.deno.com/runtime/tutorials/connecting_to_databases/) and [even use ORMs like Prisma](https://docs.deno.com/runtime/tutorials/how_to_with_npm/prisma/) with +> Deno. + +Secondly, we need our Hono server, `api/main.ts`: + +```tsx +// api/main.ts + +import { Hono } from "@hono/hono"; +import data from "./data.json" with { type: "json" }; + +const app = new Hono(); + +app.get("/", (c) => { + return c.text("Welcome to the dinosaur API!"); +}); + +app.get("/api/dinosaurs", (c) => { + return c.json(data); +}); + +app.get("/api/dinosaurs/:dinosaur", (c) => { + if (!c.req.param("dinosaur")) { + return c.text("No dinosaur name provided."); + } + + const dinosaur = data.find((item) => + item.name.toLowerCase() === c.req.param("dinosaur").toLowerCase() + ); + + console.log(dinosaur); + + if (dinosaur) { + return c.json(dinosaur); + } else { + return c.notFound(); + } +}); + +Deno.serve(app.fetch); +``` + +This Hono server provides two API endpoints: + +- `GET /api/dinosaurs` to fetch all dinosaurs, and +- `GET /api/dinosaurs/:dinosaur` to fetch a specific dinosaur by name + +This server will be started on `localhost:8000` when we run `deno task dev`. + +Finally, before we start building out the frontend, let's update our +`vite.config.ts` file with the below, especially the `server.proxy`, which +informs our SolidJS frontend where to locate the API endpoint. + +```tsx +// vite.config.ts +import { defineConfig } from "vite"; +import solid from "vite-plugin-solid"; + +export default defineConfig({ + plugins: [solid()], + server: { + proxy: { + "/api": { + target: "http://localhost:8000", + changeOrigin: true, + }, + }, + }, +}); +``` + +## Create our SolidJS frontend + +Before we begin building out the frontend components, let's quickly define the +`Dino` type in `src/types.ts`: + +```tsx +// src/types.ts +export type Dino = { + name: string; + description: string; +}; +``` + +The `Dino` type interface ensures type safety throughout our application, +defining the shape of our dinosaur data and enabling TypeScript's static type +checking. + +Next, let's set up our frontend to receive that data. We're going to have two +pages: + +- `Index.tsx` +- `Dinosaur.tsx` + +Here's the code for the `src/pages/Index.tsx` page: + +```tsx +// src/pages/Index.tsx + +import { createSignal, For, onMount } from "solid-js"; +import { A } from "@solidjs/router"; +import type { Dino } from "../types.ts"; + +export default function Index() { + const [dinosaurs, setDinosaurs] = createSignal([]); + + onMount(async () => { + try { + const response = await fetch("/api/dinosaurs"); + const allDinosaurs = (await response.json()) as Dino[]; + setDinosaurs(allDinosaurs); + console.log("Fetched dinosaurs:", allDinosaurs); + } catch (error) { + console.error("Failed to fetch dinosaurs:", error); + } + }); + + return ( +
+

Welcome to the Dinosaur app

+

Click on a dinosaur below to learn more.

+ + {(dinosaur) => ( + + {dinosaur.name} + + )} + +
+ ); +} +``` + +When using SolidJS, there are a few key differences to React to be aware of: + +1. We use SolidJS-specific primitives: + - `createSignal` instead of `useState` + - `createEffect` instead of `useEffect` + - `For` component instead of `map` + - `A` component instead of `Link` +2. SolidJS components use fine-grained reactivity, so we call signals as + functions, e.g. `dinosaur()` instead of just `dinosaur` +3. The routing is handled by `@solidjs/router` instead of `react-router-dom` +4. Component imports use Solid's + [`lazy`](https://docs.solidjs.com/reference/component-apis/lazy) for code + splitting + +The `Index` page uses SolidJS's `createSignal` to manage the list of dinosaurs +and `onMount` to fetch the data when the component loads. We use the `For` +component, which is SolidJS's efficient way of rendering lists, rather than +using JavaScript's map function. The `A` component from `@solidjs/router` +creates client-side navigation links to individual dinosaur pages, preventing +full page reloads. + +Now the individual dinosaur data page at `src/pages/Dinosaur.tsx`: + +```tsx +// src/pages/Dinosaur.tsx + +import { createSignal, onMount } from "solid-js"; +import { A, useParams } from "@solidjs/router"; +import type { Dino } from "../types.ts"; + +export default function Dinosaur() { + const params = useParams(); + const [dinosaur, setDinosaur] = createSignal({ + name: "", + description: "", + }); + + onMount(async () => { + const resp = await fetch(`/api/dinosaurs/${params.selectedDinosaur}`); + const dino = (await resp.json()) as Dino; + setDinosaur(dino); + console.log("Dinosaur", dino); + }); + + return ( +
+

{dinosaur().name}

+

{dinosaur().description}

+ Back to all dinosaurs +
+ ); +} +``` + +The `Dinosaur` page demonstrates SolidJS's approach to dynamic routing by using +`useParams` to access the URL parameters. It follows a similar pattern to the +`Index` page, using `createSignal` for state management and `onMount` for data +fetching, but focuses on a single dinosaur's details. This `Dinosaur` component +also shows how to access signal values in the template by calling them as +functions (e.g., `dinosaur().name`), which is a key difference from React's +state management. + +Finally, to tie it all together, we'll update the `App.tsx` file, which will +serve both the `Index` and `Dinosaur` pages as components. The `App` component +sets up our routing configuration using `@solidjs/router`, defining two main +routes: the index route for our dinosaur list and a dynamic route for individual +dinosaur pages. The `:selectedDinosaur` parameter in the route path creates a +dynamic segment that matches any dinosaur name in the URL. + +```tsx +// src/App.tsx + +import { Route, Router } from "@solidjs/router"; +import Index from "./pages/Index.tsx"; +import Dinosaur from "./pages/Dinosaur.tsx"; +import "./App.css"; + +const App = () => { + return ( + + + + + ); +}; + +export default App; +``` + +Finally, this `App` component will be called from our main index: + +```tsx +// src/index.tsx + +import { render } from "solid-js/web"; +import App from "./App.tsx"; +import "./index.css"; + +const wrapper = document.getElementById("root"); + +if (!wrapper) { + throw new Error("Wrapper div not found"); +} + +render(() => , wrapper); +``` + +The entry point of our application mounts the App component to the DOM using +SolidJS's `render` function. It includes a safety check to ensure the root +element exists before attempting to render, providing better error handling +during initialization. + +Now, let's run `deno task dev` to start both the frontend and backend together: + +
+ + + +
+ +## Next steps + +🦕 Now you can build and run a SolidJS app with Deno! Here are some ways you +could enhance your dinosaur application: + +- Add persistent data store + [using a database like Postgres or MongoDB](https://docs.deno.com/runtime/tutorials/connecting_to_databases/) + and an ORM like [Drizzle](https://deno.com/blog/build-database-app-drizzle) or + [Prisma](https://docs.deno.com/runtime/tutorials/how_to_with_npm/prisma/) +- Implement global state using SolidJS's + [`createContext`](https://docs.solidjs.com/reference/component-apis/create-context) + for sharing data between components +- Add loading states using + [`createResource`](https://docs.solidjs.com/reference/basic-reactivity/create-resource)'s + loading property +- Implement route-based code splitting with + [`lazy`](https://docs.solidjs.com/reference/component-apis/lazy) imports +- Use `Index` component for more efficient list rendering +- Deploy your app to + [AWS](https://docs.deno.com/runtime/tutorials/aws_lightsail/), + [Digital Ocean](https://docs.deno.com/runtime/tutorials/digital_ocean/), or + [Google Cloud Run](https://docs.deno.com/runtime/tutorials/google_cloud_run/) + +The combination of SolidJS's unique reactive primitives, true DOM +reconciliation, and Deno's modern runtime provides an incredibly efficient +foundation for web development. With no Virtual DOM overhead and granular +updates only where needed, your application can achieve optimal performance +while maintaining clean, readable code.