From 2c0e1d94ea1130beee25e1467362e7b6b588836e Mon Sep 17 00:00:00 2001 From: Kosei Masuda Date: Mon, 23 Feb 2026 01:24:40 +0900 Subject: [PATCH] Fixed tutorial so it runs correctly --- ...your_first_solid_app_with_ldo_and_react.md | 214 ++++++++++++------ 1 file changed, 148 insertions(+), 66 deletions(-) diff --git a/docs/guides/building_your_first_solid_app_with_ldo_and_react.md b/docs/guides/building_your_first_solid_app_with_ldo_and_react.md index 54c6960..8d93298 100644 --- a/docs/guides/building_your_first_solid_app_with_ldo_and_react.md +++ b/docs/guides/building_your_first_solid_app_with_ldo_and_react.md @@ -47,8 +47,10 @@ Now, let's set up a basic component structure for our app. We'll create five com **src/App.tsx**: The main application component. ```typescript -import React, { FunctionComponent } from 'react'; -import { Header } from './Header';import { Blog } from './Blog'; +import React from 'react'; +import type { FunctionComponent } from 'react'; +import { Header } from './Header'; +import { Blog } from './Blog'; const App: FunctionComponent = () => { return ( @@ -65,7 +67,7 @@ export default App; **src/Header.tsx**: A header for handling login. ```typescript -import { FunctionComponent } from "react"; +import type { FunctionComponent } from "react"; export const Header: FunctionComponent = () => { return ( @@ -80,7 +82,7 @@ export const Header: FunctionComponent = () => { **src/Blog.tsx**: The main component for the blog timeline. ```typescript -import { FunctionComponent } from "react"; +import type { FunctionComponent } from "react"; import { MakePost } from "./MakePost"; import { Post } from "./Post"; @@ -98,14 +100,15 @@ export const Blog: FunctionComponent = () => { **src/MakePost.tsx**: A form for creating new posts. ```typescript -import { FormEvent, FunctionComponent, useCallback, useState } from "react"; +import { useCallback, useState } from "react"; +import type { FunctionComponent } from "react"; export const MakePost: FunctionComponent = () => { const [message, setMessage] = useState(""); const [selectedFile, setSelectedFile] = useState(); const onSubmit = useCallback( - async (e: FormEvent) => { + async (e: React.SubmitEvent) => { e.preventDefault(); // We will add upload functionality here console.log("Submitting:", { message, selectedFile }); @@ -135,7 +138,7 @@ export const MakePost: FunctionComponent = () => { **src/Post.tsx**: A component to render a single post. ```typescript -import { FunctionComponent } from "react"; +import type { FunctionComponent } from "react"; export const Post: FunctionComponent = () => { return ( @@ -153,7 +156,7 @@ Start your application by running npm run dev. You should see a basic, unstyled With the basic structure in place, let's install LDO and connect our app to the Solid ecosystem. ```bash -npm install @ldo/solid-react +npm install @ldo/solid-react @ldo/solid ``` This library provides React hooks and components that make Solid development much easier. To use them, we need to wrap our application in a BrowserSolidLdoProvider. You can learn more about the hooks and utilities it provides in the [**LDO API Documentation**](https://ldo.js.org/latest/api/). @@ -161,7 +164,8 @@ This library provides React hooks and components that make Solid development muc Modify **src/App.tsx**: ```typescript -import React, { FunctionComponent } from 'react'; +import React from 'react'; +import type { FunctionComponent } from 'react'; import { Header } from './Header'; import { Blog } from './Blog'; import { BrowserSolidLdoProvider } from '@ldo/solid-react'; @@ -188,7 +192,8 @@ Let's update **src/Header.tsx** to handle login and logout. ```typescript import { useSolidAuth } from "@ldo/solid-react"; -import { FunctionComponent, useState } from "react"; +import { useState } from "react"; +import type { FunctionComponent } from "react"; export const Header: FunctionComponent = () => { const { session, login, logout } = useSolidAuth(); @@ -229,7 +234,7 @@ Here's what's happening: Next, let's update **src/Blog.tsx** to only show the blog content if the user is logged in. ```typescript -import { FunctionComponent } from "react"; +import type { FunctionComponent } from "react"; import { MakePost } from "./MakePost"; import { Post } from "./Post"; import { useSolidAuth } from "@ldo/solid-react"; @@ -297,6 +302,39 @@ npm run build:ldo This command reads your .shex files and generates corresponding code in the .ldo folder, which we'll use in the next step. +## Troubleshooting: TypeScript Import Errors + +Some files generated by the `npm run build:ldo` command (located in `src/.ldo/`) may fail to compile or run when using modern ESM toolchains such as Vite. + +You may encounter errors such as: + +* Failed to resolve import +* Module not found +* Vite import analysis errors + +### Cause + +The code generation step performed by `npm run build:ldo` may create imports that are used **only for types**, but are written as normal runtime imports. + +Because bundlers resolve modules **before** TypeScript removes type-only imports, this can cause runtime resolution failures. + +### Required Fix + +In files inside `src/.ldo/`, the following imports **must be converted to `import type`**: + +```ts +import type { Schema } from "shexj"; +import type { ShapeType } from "@ldo/ldo"; +import type { SolidProfile } from "./solidProfile.typings"; +import type { LdoJsonldContext, LdSet } from "@ldo/ldo"; +// needed later +import type { PostSh } from "./post.typings"; +``` + +These modules are used purely for typing and should never be loaded at runtime. + +> This behavior originates from the generated files, not from your project configuration. + ## 6. Fetching and Displaying Profile Data Let's make our header more personal by displaying the user's name instead of their WebID. We can do this by fetching their profile data from their Pod. @@ -304,9 +342,10 @@ Let's make our header more personal by displaying the user's name instead of the Update **src/Header.tsx** to use the useResource and useSubject hooks. ```typescript -import { FunctionComponent, useState } from "react"; +import { useState } from "react"; +import type { FunctionComponent } from "react"; import { useResource, useSolidAuth, useSubject } from "@ldo/solid-react"; -import { SolidProfileShapeShapeType } from "./.ldo/solidProfile.shapeTypes"; +import { SolidProfileShapeType } from "./.ldo/solidProfile.shapeTypes"; export const Header: FunctionComponent = () => { const { session, login, logout } = useSolidAuth(); @@ -315,10 +354,10 @@ export const Header: FunctionComponent = () => { // Fetch the resource at the user's WebID const webIdResource = useResource(session.webId); // Interpret the WebID resource using the SolidProfile shape - const profile = useSubject(SolidProfileShapeShapeType, session.webId); + const profile = useSubject(SolidProfileShapeType, session.webId); // Determine what name to display - const loggedInName = webIdResource?.isReading() + const loggedInName = webIdResource?.isLoading() ? "Loading..." : profile?.fn || profile?.name || session.webId; @@ -374,10 +413,10 @@ PREFIX schema: ex:PostSh { a [schema:SocialMediaPosting schema:CreativeWork schema:Thing] ; - schema:articleBody> xsd:string? + schema:articleBody xsd:string? // rdfs:label '''articleBody''' // rdfs:comment '''The actual body of the article. ''' ; - schema:uploadDate> xsd:date + schema:uploadDate xsd:date // rdfs:label '''uploadDate''' // rdfs:comment '''Date when this media object was uploaded to this site.''' ; schema:image IRI ? @@ -387,8 +426,6 @@ ex:PostSh { // rdfs:label '''publisher''' // rdfs:comment '''The publisher of the creative work.''' ; } -// rdfs:label '''SocialMediaPost''' -// rdfs:comment '''A post to a social media platform, including blog posts, tweets, Facebook posts, etc.''' ``` This shape defines a SocialMediaPosting with a body, a date, and an optional image. @@ -406,49 +443,69 @@ A common question in Solid is: "Where do I save my app's data?" One possibility Let's update **src/Blog.tsx** to find the root container and create a folder for our app. ```typescript -import { FunctionComponent, useEffect, useState, Fragment } from "react"; +import { useEffect, useState, Fragment } from "react"; +import type { FunctionComponent } from "react"; import { MakePost } from "./MakePost"; import { Post } from "./Post"; import { useLdo, useResource, useSolidAuth, useSubject } from "@ldo/solid-react"; -import { SolidProfileShapeShapeType } from "./.ldo/solidProfile.shapeTypes"; -import { Container, ContainerUri } from "@ldo/solid"; +import { SolidProfileShapeType } from "./.ldo/solidProfile.shapeTypes"; +import type { SolidContainer, SolidContainerUri } from "@ldo/connected-solid"; export const Blog: FunctionComponent = () => { const { session } = useSolidAuth(); - const profile = useSubject(SolidProfileShapeShapeType, session.webId); + const profile = useSubject(SolidProfileShapeType, session.webId); const { getResource } = useLdo(); - const [mainContainerUri, setMainContainerUri] = useState(); + const [mainContainerUri, setMainContainerUri] = useState(); useEffect(() => { - if (profile?.storage?.[0]?.["@id"]) { - const storageUri = profile.storage[0]["@id"] as ContainerUri; - const appContainerUri = `${storageUri}my-solid-app/`; + const run = async () => { + const storageId = + profile?.storage?.toArray()?.[0]?.["@id"]; + + if (!storageId) return; + + const storageUri = storageId as SolidContainerUri; + const appContainerUri = + `${storageUri}my-solid-app/` as SolidContainerUri; + setMainContainerUri(appContainerUri); - // Create the container if it doesn't exist + const appContainer = getResource(appContainerUri); - appContainer.createIfAbsent(); - } + + if ("createIfAbsent" in appContainer) { + await appContainer.createIfAbsent(); + } + }; + + run(); }, [profile, getResource]); const mainContainer = useResource(mainContainerUri); + if (!mainContainer) { + return

Loading...

; + } + if (!session.isLoggedIn) { return

Please log in to see your blog.

; } + const isSolidContainer = (res: any): res is SolidContainer => + res?.children && typeof res.children === "function"; return (
- + {isSolidContainer(mainContainer) && }
- {mainContainer - ?.children() - .filter((child): child is Container => child.type === "container") - .map((child) => ( - - -
-
- ))} + {isSolidContainer(mainContainer) && + mainContainer + .children() + .filter((child: any) => child.type === "SolidContainer") + .map((child: any) => ( + + +
+
+ ))}
); }; @@ -467,13 +524,14 @@ We also started logic to render posts. mainContainer.children() gets a list of a Now let's wire up the **src/MakePost.tsx** component to actually create data. ```typescript -import { FormEvent, FunctionComponent, useCallback, useState } from "react"; -import { Container, Leaf, LeafUri } from "@ldo/solid"; +import { useCallback, useState } from "react"; +import type { FunctionComponent } from "react"; import { useLdo, useSolidAuth } from "@ldo/solid-react"; import { v4 as uuid } from "uuid"; -import { PostShapeShapeType } from "./.ldo/post.shapeTypes"; +import { PostShShapeType } from "./.ldo/post.shapeTypes"; +import type { SolidLeaf, SolidContainer, SolidLeafUri } from "@ldo/connected-solid"; -export const MakePost: FunctionComponent<{ mainContainer?: Container }> = ({ +export const MakePost: FunctionComponent<{ mainContainer?: SolidContainer }> = ({ mainContainer, }) => { const { session } = useSolidAuth(); @@ -481,8 +539,12 @@ export const MakePost: FunctionComponent<{ mainContainer?: Container }> = ({ const [message, setMessage] = useState(""); const [selectedFile, setSelectedFile] = useState(); + function isSolidLeaf(res: any): res is SolidLeaf { + return !!res && typeof res === "object" && res.type === "SolidLeaf"; + } + const onSubmit = useCallback( - async (e: FormEvent) => { + async (e: React.SyntheticEvent) => { e.preventDefault(); if (!mainContainer || !session.webId) return; @@ -492,10 +554,10 @@ export const MakePost: FunctionComponent<{ mainContainer?: Container }> = ({ const postContainer = postContainerResult.resource; // 2. Upload the image file (if one was selected) - let uploadedImage: Leaf | undefined; + let uploadedImage: SolidLeaf | undefined; if (selectedFile) { const imageResult = await postContainer.uploadChildAndOverwrite( - selectedFile.name as LeafUri, + selectedFile.name as SolidLeafUri, selectedFile, selectedFile.type ); @@ -505,7 +567,10 @@ export const MakePost: FunctionComponent<{ mainContainer?: Container }> = ({ // 3. Create the structured data (index.ttl) const indexResource = postContainer.child("index.ttl"); - const post = createData(PostShapeShapeType, indexResource.uri, indexResource); + if (!isSolidLeaf(indexResource)) { + return alert("Index resource is not a SolidLeaf"); + } + const post = createData(PostShShapeType, indexResource.uri, indexResource); post.articleBody = message; post.uploadDate = new Date().toISOString(); if (uploadedImage) { @@ -540,13 +605,15 @@ export const MakePost: FunctionComponent<{ mainContainer?: Container }> = ({ ); }; + + ``` This is the most complex step, so let's break it down: 1. **Create Post Container:** We create a new, uniquely named sub-container inside our main app container to hold this specific post. 2. **Upload Image:** If the user selected a file, we use uploadChildAndOverwrite to save it inside the new post's container. This is for "unstructured" data. -3. **Create Structured Data:** We define where our structured data will live (index.ttl). Then, createData(PostShapeShapeType, ...) gives us a special LDO object (post) that conforms to our PostShape. We can then set its properties (articleBody, uploadDate, image) like a normal object. +3. **Create Structured Data:** We define where our structured data will live (index.ttl). Then, createData(PostShShapeType, ...) gives us a special LDO object (post) that conforms to our PostShape. We can then set its properties (articleBody, uploadDate, image) like a normal object. 4. **Commit Data:** commitData(post) takes our local changes and sends them to the Solid Pod, creating the index.ttl file with the correct RDF data. ## 8. Displaying the Post Content @@ -554,44 +621,59 @@ This is the most complex step, so let's break it down: Finally, let's update **src/Post.tsx** to fetch and display the data for each post. ```typescript -import { FunctionComponent, useMemo, useCallback } from "react"; -import { ContainerUri, LeafUri } from "@ldo/solid"; +import { useMemo, useCallback } from "react"; +import type { FunctionComponent } from "react"; import { useLdo, useResource, useSubject } from "@ldo/solid-react"; -import { PostShapeShapeType } from "./.ldo/post.shapeTypes"; +import { PostShShapeType } from "./.ldo/post.shapeTypes"; -export const Post: FunctionComponent<{ postContainerUri: ContainerUri }> = ({ - postContainerUri, -}) => { +type PostProps = { + postContainerUri: string; +}; + +export const Post: FunctionComponent = ({ postContainerUri }) => { const postIndexUri = `${postContainerUri}index.ttl`; + const postResource = useResource(postIndexUri); - const post = useSubject(PostShapeShapeType, postIndexUri); + const post = useSubject(PostShShapeType, postIndexUri); const { getResource } = useLdo(); - const imageResource = useResource(post?.image?.["@id"] as LeafUri | undefined); + const imageResource = useResource(post?.image?.["@id"]); - // Convert the fetched image blob into a URL for the tag const imageUrl = useMemo(() => { - if (imageResource?.isBinary()) { - return URL.createObjectURL(imageResource.getBlob()!); + const res = imageResource as any; + if (res?.isBinary?.()) { + return URL.createObjectURL(res.getBlob()); } }, [imageResource]); const deletePost = useCallback(async () => { - // We can just delete the entire container for the post - const postContainer = getResource(postContainerUri); - await postContainer.delete(); + const postContainer = getResource(postContainerUri) as any; + await postContainer.delete?.(); }, [postContainerUri, getResource]); - if (postResource?.isReading()) return

Loading post...

; + if ((postResource as any)?.isReading?.()) return

Loading post...

; if (!post) return null; return (

{post.articleBody}

- {imageUrl && Post} + + {imageUrl && ( + Post + )} +

- Posted on: {new Date(post.uploadDate!).toLocaleString()} + + Posted on: {post.uploadDate + ? new Date(post.uploadDate).toLocaleString() + : ""} +

+
);