تخطَّ إلى المحتوى

SolidJS

إطار عمل JavaScript تفاعلي دقيق لواجهات المستخدم بدون DOM افتراضي وأداء استثنائي.

الأمرالوصف
npx degit solidjs/templates/ts my-appإنشاء مشروع SolidJS بـ TypeScript
npx degit solidjs/templates/js my-appإنشاء مشروع SolidJS بـ JavaScript
npm create solid@latestإنشاء مشروع باستخدام SolidStart CLI
npm install solid-jsتثبيت SolidJS في مشروع موجود
npm install solid-startتثبيت إطار SolidStart الشامل
الأمرالوصف
npm run devتشغيل خادم التطوير مع إعادة التحميل التلقائي
npm run buildبناء للإنتاج
npm run serveمعاينة بناء الإنتاج محلياً
npm run startتشغيل خادم SolidStart
# SolidJS أساسي (Vite)
npx degit solidjs/templates/ts my-app
cd my-app && npm install

# SolidStart (إطار عمل شامل)
npm create solid@latest my-app
# اختر قالباً: bare, with-auth, with-mdx, with-prisma, with-tailwindcss

# قوالب المجتمع
npx degit solidjs/templates/ts-windicss my-app    # WindiCSS
npx degit solidjs/templates/ts-unocss my-app      # UnoCSS
npx degit solidjs/templates/ts-bootstrap my-app    # Bootstrap
npx degit solidjs/templates/ts-sass my-app         # Sass
الأمرالوصف
function App() { return <div>Hello</div> }تعريف مكون أساسي
<MyComponent name="value" />تمرير الخصائص لمكون
props.childrenالوصول لمحتوى الأبناء
<div class={styles.container}>تطبيق فئة CSS (استخدم class وليس className)
<div classList={{ active: isActive() }}>فئات CSS شرطية
<div style={{ color: 'red', 'font-size': '14px' }}>تطبيق أنماط مضمنة
<div innerHTML={htmlString} />تعيين محتوى HTML خام
<div ref={myRef}>إرفاق مرجع DOM
الأمرالوصف
<div onClick={handler}>إرفاق حدث مفوّض
<div on:click={handler}>إرفاق حدث DOM أصلي (يتجاوز التفويض)
<div onInput={(e) => setValue(e.target.value)}>معالجة أحداث الإدخال
<div on:keydown={(e) => handleKey(e)}>معالجة أحداث لوحة المفاتيح أصلياً
<div onClick={[handler, data]}>تمرير بيانات مع معالج الحدث
import { type Component, type ParentComponent } from 'solid-js'

// مكون بأنواع محددة مع خصائص
interface UserProps {
  name: string
  age: number
  role?: string
}

const UserCard: Component<UserProps> = (props) => {
  return (
    <div class="card">
      <h2>{props.name}</h2>
      <p>العمر: {props.age}</p>
      <p>الدور: {props.role ?? 'عضو'}</p>
    </div>
  )
}

// مكون أب (يقبل أبناء)
const Layout: ParentComponent = (props) => {
  return (
    <div class="layout">
      <header>تطبيقي</header>
      <main>{props.children}</main>
      <footer>التذييل</footer>
    </div>
  )
}

// تقسيم الخصائص لإعادة التوجيه
import { splitProps } from 'solid-js'

const Button: Component<ButtonProps> = (props) => {
  const [local, rest] = splitProps(props, ['variant', 'size'])
  return (
    <button
      class={`btn btn-${local.variant} btn-${local.size}`}
      {...rest}
    />
  )
}

// دمج الخصائص الافتراضية
import { mergeProps } from 'solid-js'

const Alert: Component<AlertProps> = (rawProps) => {
  const props = mergeProps({ type: 'info', dismissible: false }, rawProps)
  return <div class={`alert alert-${props.type}`}>{props.children}</div>
}
الأمرالوصف
const [count, setCount] = createSignal(0)إنشاء إشارة تفاعلية
count()قراءة قيمة الإشارة (يجب استدعاؤها كدالة)
setCount(5)تعيين الإشارة لقيمة محددة
setCount(prev => prev + 1)تحديث الإشارة بالقيمة السابقة
const [name, setName] = createSignal<string>()إنشاء إشارة بنوع محدد

التأثيرات والقيم المحسوبة

Section titled “التأثيرات والقيم المحسوبة”
الأمرالوصف
createEffect(() => console.log(count()))تشغيل تأثير جانبي عند تغير الإشارة
createEffect(on(count, (v) => log(v)))تتبع إشارة محددة بشكل صريح
createEffect(on([a, b], ([a, b]) => ...))تتبع إشارات متعددة بشكل صريح
const double = createMemo(() => count() * 2)إنشاء قيمة مشتقة/محسوبة
createRenderEffect(() => ...)تأثير يعمل قبل رسم DOM
createComputed(() => ...)تأثير متزامن أثناء التصيير
الأمرالوصف
onMount(() => { ... })تشغيل مرة واحدة عند تحميل المكون
onCleanup(() => { ... })تشغيل التنظيف عند إزالة المكون
batch(() => { setA(1); setB(2) })تجميع تحديثات إشارات متعددة
untrack(() => value())قراءة إشارة دون تتبع التبعية
import { createSignal, createEffect, createMemo, on, batch, untrack } from 'solid-js'

function Counter() {
  const [count, setCount] = createSignal(0)
  const [step, setStep] = createSignal(1)

  // قيمة مشتقة (محفوظة، تُعاد حسابها فقط عند تغير count)
  const doubled = createMemo(() => count() * 2)
  const isEven = createMemo(() => count() % 2 === 0)

  // تأثير: يعمل عند تغير أي إشارة يتم الوصول إليها
  createEffect(() => {
    console.log(`العدد الآن: ${count()}`)
    // يتتبع count() كتبعية تلقائياً
  })

  // تتبع صريح باستخدام on()
  createEffect(on(count, (value, prev) => {
    console.log(`تغير من ${prev} إلى ${value}`)
  }, { defer: true })) // defer: تخطي التشغيل الأولي

  // تجميع التحديثات (إعادة تصيير واحدة)
  const reset = () => batch(() => {
    setCount(0)
    setStep(1)
  })

  // القراءة دون إنشاء تبعية
  const logWithoutTracking = () => {
    const current = untrack(() => count())
    console.log('لقطة:', current)
  }

  return (
    <div>
      <p>العدد: {count()} (مضاعف: {doubled()}, زوجي: {String(isEven())})</p>
      <button onClick={() => setCount(c => c + step())}>
        أضف {step()}
      </button>
      <button onClick={reset}>إعادة تعيين</button>
    </div>
  )
}
الأمرالوصف
<Show when={loggedIn()} fallback={<Login/>}>التصيير الشرطي
<For each={items()}>{(item) => <li>{item}</li>}</For>تصيير قائمة (بمفتاح المرجع)
<Index each={items()}>{(item, i) => <li>{item()}</li>}</Index>تصيير قائمة (بمفتاح الفهرس)
<Switch><Match when={a()}>A</Match></Switch>تصيير Switch/case
<ErrorBoundary fallback={err => <p>{err}</p>}>التقاط أخطاء التصيير
<Suspense fallback={<Loading/>}>عرض بديل أثناء التحميل غير المتزامن
<Dynamic component={MyComp} />تصيير مكون ديناميكي
<Portal mount={document.body}>التصيير في عقدة DOM مختلفة
import { Show, For, Index, Switch, Match, Suspense, ErrorBoundary } from 'solid-js'

function Dashboard() {
  const [user, setUser] = createSignal(null)
  const [items, setItems] = createSignal([
    { id: 1, name: 'ألفا', status: 'نشط' },
    { id: 2, name: 'بيتا', status: 'غير نشط' },
  ])
  const [view, setView] = createSignal('list')

  return (
    <div>
      {/* Show: تصيير شرطي مع بديل */}
      <Show when={user()} fallback={<p>يرجى تسجيل الدخول</p>}>
        {(u) => <p>مرحباً، {u().name}!</p>}
      </Show>

      {/* For: بمفتاح المرجع (الأفضل للكائنات) */}
      <For each={items()}>
        {(item, index) => (
          <div>
            <span>{index() + 1}. {item.name}</span>
            <span> ({item.status})</span>
          </div>
        )}
      </For>

      {/* Switch/Match: شروط متعددة */}
      <Switch fallback={<p>عرض غير معروف</p>}>
        <Match when={view() === 'list'}>
          <ListView />
        </Match>
        <Match when={view() === 'grid'}>
          <GridView />
        </Match>
        <Match when={view() === 'table'}>
          <TableView />
        </Match>
      </Switch>

      {/* ErrorBoundary: التقاط الأخطاء في الشجرة الفرعية */}
      <ErrorBoundary fallback={(err, reset) => (
        <div>
          <p>خطأ: {err.message}</p>
          <button onClick={reset}>حاول مرة أخرى</button>
        </div>
      )}>
        <RiskyComponent />
      </ErrorBoundary>
    </div>
  )
}
الأمرالوصف
const [store, setStore] = createStore({})إنشاء مخزن تفاعلي
setStore('name', 'Alice')تحديث خاصية المخزن بالمسار
setStore('users', 0, 'name', 'Bob')تحديث خاصية مخزن متداخلة
setStore('list', l => [...l, item])إضافة عنصر لمصفوفة المخزن
setStore('list', i => i.id === 1, 'done', true)تحديث عناصر المصفوفة المطابقة
produce(s => { s.count++ })تحديثات المخزن بأسلوب قابل للتغيير
reconcile(newData)استبدال بيانات المخزن بكفاءة
unwrap(store)الحصول على البيانات الخام غير الملفوفة بـ proxy
import { createStore, produce, reconcile, unwrap } from 'solid-js/store'

interface Todo {
  id: number
  text: string
  completed: boolean
}

interface AppState {
  todos: Todo[]
  filter: 'all' | 'active' | 'completed'
  user: { name: string; settings: { theme: string } }
}

function TodoApp() {
  const [state, setState] = createStore<AppState>({
    todos: [],
    filter: 'all',
    user: { name: 'Alice', settings: { theme: 'dark' } },
  })

  // إضافة عنصر
  const addTodo = (text: string) => {
    setState('todos', todos => [
      ...todos,
      { id: Date.now(), text, completed: false }
    ])
  }

  // تبديل عنصر محدد بالمطابقة
  const toggleTodo = (id: number) => {
    setState('todos', todo => todo.id === id, 'completed', c => !c)
  }

  // حذف عنصر (التصفية تنتج مصفوفة جديدة)
  const deleteTodo = (id: number) => {
    setState('todos', todos => todos.filter(t => t.id !== id))
  }

  // تحديثات بأسلوب قابل للتغيير باستخدام produce
  const clearCompleted = () => {
    setState(produce((s) => {
      s.todos = s.todos.filter(t => !t.completed)
    }))
  }

  // تحديث متداخل عميق
  const setTheme = (theme: string) => {
    setState('user', 'settings', 'theme', theme)
  }

  // استبدال بيانات المخزن بالكامل (reconcile يقارن بكفاءة)
  const loadFromServer = async () => {
    const data = await fetch('/api/todos').then(r => r.json())
    setState('todos', reconcile(data))
  }

  return (/* ... */)
}
الأمرالوصف
const [data] = createResource(fetchFn)إنشاء مورد غير متزامن
const [data] = createResource(id, fetchFn)مورد مع إشارة مصدر تفاعلية
data()الوصول لبيانات المورد
data.loadingالتحقق مما إذا كان المورد قيد التحميل
data.errorالوصول لخطأ المورد
data.latestالحصول على آخر قيمة محلولة (تبقى بعد إعادة الجلب)
data.stateالحصول على حالة المورد (‘unresolved’, ‘pending’, ‘ready’, ‘errored’)
const { refetch } = dataإعادة الجلب يدوياً
createResource(source, fetcher, { initialValue })مورد بقيمة أولية
import { createResource, createSignal, Suspense, ErrorBoundary } from 'solid-js'

interface User {
  id: number
  name: string
  email: string
}

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) throw new Error(`المستخدم ${id} غير موجود`)
  return res.json()
}

function UserProfile() {
  const [userId, setUserId] = createSignal(1)

  // المورد يعيد الجلب تلقائياً عند تغير userId()
  const [user, { refetch, mutate }] = createResource(userId, fetchUser)

  // تحديث متفائل
  const updateName = async (newName: string) => {
    const prev = user()
    mutate({ ...prev!, name: newName }) // متفائل
    try {
      await fetch(`/api/users/${userId()}`, {
        method: 'PATCH',
        body: JSON.stringify({ name: newName }),
      })
    } catch {
      mutate(prev) // التراجع عند الخطأ
    }
  }

  return (
    <ErrorBoundary fallback={<p>فشل تحميل المستخدم</p>}>
      <Suspense fallback={<p>جاري تحميل المستخدم...</p>}>
        <div>
          <h2>{user()?.name}</h2>
          <p>{user()?.email}</p>
          <p>الحالة: {user.state}</p>
          <button onClick={refetch} disabled={user.loading}>
            تحديث
          </button>
          <button onClick={() => setUserId(id => id + 1)}>
            المستخدم التالي
          </button>
        </div>
      </Suspense>
    </ErrorBoundary>
  )
}
الأمرالوصف
npm install @solidjs/routerتثبيت موجه SolidJS
<Router><Route path="/" component={Home}/></Router>تعريف مسار أساسي
<Route path="/users/:id" component={User}/>مسار بمعامل
<Route path="/*all" component={NotFound}/>مسار شامل
const params = useParams()الوصول لمعاملات المسار
const [searchParams, setSearchParams] = useSearchParams()الوصول لمعاملات الاستعلام
const navigate = useNavigate()التنقل البرمجي
navigate('/dashboard')التنقل إلى مسار
<A href="/about">حول</A>مكون رابط التنقل
<A href="/about" activeClass="active">رابط بتنسيق نشط
import { Router, Route, A, useParams, useNavigate, useSearchParams } from '@solidjs/router'
import { lazy } from 'solid-js'

// مسارات محملة بتأخير
const Dashboard = lazy(() => import('./pages/Dashboard'))
const UserProfile = lazy(() => import('./pages/UserProfile'))
const Settings = lazy(() => import('./pages/Settings'))

function App() {
  return (
    <Router>
      <nav>
        <A href="/" activeClass="active" end>الرئيسية</A>
        <A href="/dashboard" activeClass="active">لوحة التحكم</A>
        <A href="/settings" activeClass="active">الإعدادات</A>
      </nav>
      <Route path="/" component={Home} />
      <Route path="/dashboard" component={Dashboard} />
      <Route path="/users/:id" component={UserProfile} />
      <Route path="/settings" component={Settings} />
      <Route path="/*all" component={NotFound} />
    </Router>
  )
}

// مسارات متداخلة
function App() {
  return (
    <Router>
      <Route path="/admin" component={AdminLayout}>
        <Route path="/" component={AdminDashboard} />
        <Route path="/users" component={AdminUsers} />
        <Route path="/users/:id" component={AdminUserDetail} />
      </Route>
    </Router>
  )
}
import { createContext, useContext, type ParentComponent } from 'solid-js'
import { createStore } from 'solid-js/store'

interface AuthState {
  user: { name: string; role: string } | null
  token: string | null
}

interface AuthContextValue {
  state: AuthState
  login: (token: string, user: AuthState['user']) => void
  logout: () => void
  isAuthenticated: () => boolean
}

const AuthContext = createContext<AuthContextValue>()

const AuthProvider: ParentComponent = (props) => {
  const [state, setState] = createStore<AuthState>({
    user: null,
    token: null,
  })

  const value: AuthContextValue = {
    state,
    login: (token, user) => {
      setState({ token, user })
    },
    logout: () => {
      setState({ token: null, user: null })
    },
    isAuthenticated: () => state.token !== null,
  }

  return (
    <AuthContext.Provider value={value}>
      {props.children}
    </AuthContext.Provider>
  )
}

function useAuth() {
  const context = useContext(AuthContext)
  if (!context) throw new Error('يجب استخدام useAuth داخل AuthProvider')
  return context
}

// الاستخدام
function UserMenu() {
  const auth = useAuth()
  return (
    <Show when={auth.isAuthenticated()} fallback={<LoginButton />}>
      <p>مرحباً، {auth.state.user?.name}</p>
      <button onClick={auth.logout}>تسجيل الخروج</button>
    </Show>
  )
}
الأمرالوصف
import styles from './App.module.css'استيراد وحدات CSS
<div class={styles.container}>تطبيق فئة وحدة CSS
npm install solid-styled-componentsتثبيت المكونات المنسقة لـ Solid
const Btn = styled('button')\color: red“إنشاء مكون منسق
<div class="static-class">تطبيق فئة CSS ثابتة
# تثبيت Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
export default {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  theme: { extend: {} },
  plugins: [],
}
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
الأمرالوصف
npm create solid@latestإنشاء مشروع SolidStart
"use server" directiveتحديد الدالة كخادم فقط
createServerAction$()إنشاء إجراء خادم
createRouteData()تحميل بيانات المسار على الخادم
<Title>عنوان الصفحة</Title>تعيين عنوان الصفحة (من @solidjs/meta)
<Meta name="description" content="..." />تعيين وسوم meta
// src/routes/todos.tsx
import { createAsync, query, action, redirect } from '@solidjs/router'

// استعلام الخادم (تحميل البيانات)
const getTodos = query(async () => {
  'use server'
  const db = await getDatabase()
  return db.todos.findMany()
}, 'todos')

// إجراء الخادم (التعديلات)
const addTodo = action(async (formData: FormData) => {
  'use server'
  const text = formData.get('text') as string
  const db = await getDatabase()
  await db.todos.create({ data: { text, completed: false } })
  throw redirect('/todos') // يعيد التحقق
})

export default function TodosPage() {
  const todos = createAsync(() => getTodos())

  return (
    <div>
      <form action={addTodo} method="post">
        <input name="text" placeholder="مهمة جديدة" required />
        <button type="submit">إضافة</button>
      </form>
      <Suspense fallback={<p>جاري التحميل...</p>}>
        <For each={todos()}>
          {(todo) => <p>{todo.text}</p>}
        </For>
      </Suspense>
    </div>
  )
}
npm install -D vitest @solidjs/testing-library @testing-library/jest-dom jsdom
npm install -D vite-plugin-solid
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import solid from 'vite-plugin-solid'

export default defineConfig({
  plugins: [solid()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './src/test-setup.ts',
    transformMode: { web: [/\.[jt]sx?$/] },
  },
})
import { render, screen, fireEvent } from '@solidjs/testing-library'
import { describe, it, expect } from 'vitest'
import Counter from './Counter'

describe('Counter', () => {
  it('يصيّر العدد الأولي', () => {
    render(() => <Counter initialCount={5} />)
    expect(screen.getByText('Count: 5')).toBeInTheDocument()
  })

  it('يزداد عند النقر', async () => {
    render(() => <Counter initialCount={0} />)
    const button = screen.getByRole('button', { name: /increment/i })
    fireEvent.click(button)
    expect(screen.getByText('Count: 1')).toBeInTheDocument()
  })
})
  1. استدعِ الإشارات دائماً كدوالcount() وليس count. نسيان الأقواس هو الخطأ الأكثر شيوعاً في SolidJS؛ بدونها تمرر دالة الحصول بدلاً من القيمة.

  2. لا تفكك الخصائص — التفكيك يكسر التفاعلية لأنه يقرأ القيمة مرة واحدة. استخدم props.name مباشرة أو استخدم splitProps()/mergeProps() للتعامل مع الخصائص.

  3. استخدم <For> لمصفوفات الكائنات و <Index> للقيم البسيطة<For> يفهرس بالمرجع (أفضل للكائنات التي قد تُعاد ترتيبها) بينما <Index> يفهرس بالموضع (أفضل لقوائم القيم البسيطة).

  4. فضّل المخازن للحالة المعقدة — الإشارات رائعة للقيم البسيطة، لكن للكائنات والمصفوفات المتداخلة، يوفر createStore تفاعلية دقيقة دون استبدال الكائن بالكامل.

  5. استخدم on() للتتبع الصريح — عندما تريد أن يتتبع التأثير إشارات محددة فقط (وليس كل إشارة تُقرأ بداخله)، غلّفه بـ on(signal, callback).

  6. استفد من Suspense و ErrorBoundary — غلّف المكونات غير المتزامنة بـ Suspense لحالات التحميل و ErrorBoundary لمعالجة الأخطاء بأناقة.

  7. حمّل المسارات بتأخير — استخدم lazy(() => import('./Page')) لمكونات المسارات لتمكين تقسيم الشفرة وتقليل حجم الحزمة الأولي.

  8. استخدم batch() للتحديثات المتعددة — عند تعيين عدة إشارات دفعة واحدة، غلّفها بـ batch() لتشغيل إعادة تصيير واحدة بدلاً من عدة.

  9. اجعل المكونات صغيرة — مكونات SolidJS تشغل جسمها مرة واحدة فقط (ليس عند كل تصيير مثل React)، لذا قسّم المكونات الكبيرة لتحديد نطاق التحديثات التفاعلية.

  10. استخدم أنواع TypeScript العامة مع الإشاراتcreateSignal<string | null>(null) يوفر أماناً أفضل للأنواع وإكمالاً تلقائياً لحالتك التفاعلية.

  11. افهم نموذج الترجمة — يترجم SolidJS كود JSX إلى عمليات DOM حقيقية في وقت البناء. لا يوجد DOM افتراضي، وهذا سبب سرعته ولكنه أيضاً سبب أهمية قواعد التفاعلية (عدم التفكيك، استدعاء الإشارات).

  12. استخدم untrack() باعتدال — قراءة إشارة داخل untrack() تمنع تتبع التبعيات. هذا مفيد للتسجيل أو القراءات لمرة واحدة لكنه قد يسبب أخطاء إذا أُفرط في استخدامه.