# Solid.js基础

[toc]

其实都在文档 (opens new window)。只不过看过之后写一点自己的理解,毕竟官方的例子比较少。

# 1、响应式基础

createSignal、createMemo、createEffect 语法都和React的差不多,jsx绑定也都是一样的。

import { createEffect, createMemo, createSignal, Show } from 'solid-js';
export default function Hello() {
  const [count, setCount] = createSignal(0);
  createEffect(() => {
    // 副作用
    console.log('count = ', count());
  });
  const handleClick = (e: Event) => {
    setCount(count() + 1);
  };

  const tripleCount = createMemo(() => count() * 3);
  return (
    <div>
      <p>{count()}</p>
      <p>tripleCount: {tripleCount()}</p>
      <p>
        <button onClick={handleClick}>add 1</button>
      </p>
    </div>
  );
}

# 2、流程控制

# 2.1 条件判断

Show

import { createSignal, Show } from 'solid-js';


<Show
  when={loggedIn()}
  fallback={() => <button onClick={toggle}>Log in</button>}
>
  <button onClick={toggle}>Log out</button>
</Show>

Switch, Match

import { createSignal, Show, Switch, Match } from "solid-js";

<Switch fallback={<p>{x()} is between 5 and 10</p>}>
  <Match when={x() > 10}>
    <p>{x()} is greater than 10</p>
  </Match>
  <Match when={5 > x()}>
    <p>{x()} is less than 5</p>
  </Match>
</Switch>

# 2.2 列表渲染For、Index

<For> 组件是遍历任何非原始值数组的最佳方式。

<For> 的回调函数是非跟踪的,并传递数据项和索引 Signal。index 是一个 Signal,因此它可以在移动行时独立更新.

<For>的数据项不是一个Signal。 所以针对数据项的修改往往就伴随这Dom的重新创建。

<index> 的数据项是一个Signal, 索引是固定的,索引是列表的实际键。所以针对数据项的修改会单独更新,更加高效。

看下面的例子

// @ts-nocheck
import { createSignal, For, Index } from 'solid-js';
export default function ForS() {
  const [list, setList] = createSignal([
    {
      name: 'a',
      id: '1'
    },
    {
      name: 'b',
      id: '2'
    }
  ]);
  return (
    <div>
      {/* 每次触发Input的change都会重新渲染一个input */}
      <For each={list()}>
        {(item, index) => {
          return (
            <input
              type='text'
              value={item.name}
              onChange={e => {
                setList(list => {
                  return list.map((mapItem, mapIndex) => {
                    if (mapIndex === index()) {
                      return { ...mapItem, name: e.target.value };
                    }
                    return mapItem;
                  });
                });
              }}
            />
          );
        }}
      </For>
      <hr />
      {/* 每次触发Input的change不会重新渲染一个input,只会更新input本身 */}
      <Index each={list()}>
        {(item, index) => {
          return (
            <input
              type='text'
              value={item().name}
              onChange={e => {
                setList(list => {
                  return list.map((mapItem, mapIndex) => {
                    if (mapIndex === index) {
                      return { ...mapItem, name: e.target.value };
                    }
                    return mapItem;
                  });
                });
              }}
            />
          );
        }}
      </Index>
    </div>
  );
}

个人总结: 遍历显示一些不会变更的内容时,For和Index都可以,甚至数据源都没必要创建Signal。如果数据项需要变更展示,使用Index。 常规使用For即可。

# 2.3 Dynamic动态组件

<Dynamic>组件类似于Vue的内置<component>组件,可以根据属性动态渲染

# 2.4 Portal

Portal组件就是把内容渲染到组件外部。

默认情况下,它的元素将在 document.body 下的 <div> 中呈现

# 3、生命周期钩子

onMount 组件被挂载,可以访问Dom

onCleanup 组件被销毁前的钩子

const resizeHandler = () => { }
onMount(() => {
  window.addEventListener('resize', resizeHandler)
});
onCleanup(() => {
  window.removeEventListener('resize', resizeHandler)
})

# 4、绑定

事件,内链样式,class,Ref DOM获取和转发。

# 5、指令

Solid 通过 use: 命名空间支持自定义指令。但这只是 ref 一个有用的语法糖,类似于原生的绑定,并且可以在同一个元素上有多个绑定而不会发生冲突。这可以让我们更好地利用可重用 DOM 元素行为。

// @ts-nocheck
import { createSignal, Show, onCleanup, onMount } from 'solid-js';

function onClickOutside(el: HTMLElement, accessor: Function) {
  console.debug(el, accessor);
  const onClick = (e: Event) => {
    if (!el.contains(e.target)) {
      accessor()?.();
    }
  };

  document.body.addEventListener('click', onClick);

  onCleanup(() => {
    console.debug('onCleanup');
    document.body.removeEventListener('click', onClick);
  });
}

function makeRed(el: HTMLElement, accessor: Function) {
  console.debug('makeRed');
  el.style.color = 'red';
}

function makeColor(el: HTMLElement, accessor: Function) {
  console.debug(accessor(), 'makeColor');
  el.style.color = accessor() || 'red';
}

function click(el: HTMLElement, accessor: Function) {
  const onClick = e => {
    accessor()(e);
  };
  el.addEventListener('click', onClick);
  onCleanup(() => {
    el.removeEventListener('click', onClick);
  });
}
/* 自定义指令 */
export default function directive() {
  const [show, setShow] = createSignal(false);
  return (
    <div>
      <Show when={show()} fallback={<button onClick={() => setShow(true)}>show</button>}>
        <div
          use:onClickOutside={() => setShow(false)}
          class='modal'
          style={{ border: '1px solid #ccc' }}
        >
          My Modal
        </div>
      </Show>
      <div use:makeRed={() => {}}>red</div>
      <div use:makeColor={'green'}>green</div>
      <button use:click={e => console.log('click')}>click</button>
    </div>
  );
}

# 6、props

默认属性

// @ts-nocheck
import { createSignal, mergeProps, splitProps } from 'solid-js';
interface Props {
  name?: string;
  age?: number;
}
export default function Person(props: Props) {
  // 合并属性用于设置默认值
  const mergedProps = mergeProps({ name: '匿名', age: 0 }, props);
  // 分离属性用于精准传递子组件
  const [selfProps, otherProps] = splitProps(mergedProps, ['name', 'age']);
  return (
    <ul style={{ width: '200px', border: '1px solid #aaa' }}>
      <li>
        <h4>个人信息</h4>
      </li>
      <li>name: {selfProps.name}</li>
      <li>age: {selfProps.age}</li>
      <li>
        other:
        <pre>{JSON.stringify(otherProps, null, 2)}</pre>
      </li>
    </ul>
  );
}

// @ts-nocheck
import { createSignal, onMount } from 'solid-js';

import Person from '~/components/Person.tsx';
export default function Props() {
  const [name, setName] = createSignal('');
  onMount(() => {
    setTimeout(() => {
      setName('James');
    }, 1000);
  });
  return (
    <div>
      <Person name='Wade' age='27'></Person>
      <Person></Person>
      <Person name={name()} style="color: red" title="Person"></Person>
    </div>
  );
}

# 7、children 函数

主要用于获取所有后代的DOM

import { children, createEffect, createSignal, For } from 'solid-js';

function ColoredList(props: any) {
  // const c = children(() => props.children);
  const c = children(() => props.children);
  // c() 返回的是children的子元素列表
  createEffect(() => {
    (c() as Array<HTMLElement>).forEach(el => {
      el.style.color = props.color;
    });
  });
  return <>{c()}</>;
}

export default function ChildrenX() {
  const [color, setColor] = createSignal('red');
  return (
    <div>
      <ColoredList color={color()}>
        <For each={['A', 'B', 'C']}>
          {(item, index) => (
            <p>
              {item}-{index()}
            </p>
          )}
        </For>
      </ColoredList>
      <input
        type='color'
        value={color()}
        onInput={e => {
          setColor(e.currentTarget.value);
        }}
      />
    </div>
  );
}

# 8、内嵌响应式

就是可以把Signal 放到数据结构内部, 实现更细颗粒度的更新。

说白了你可以把每一个DOM节点都绑定一个signal。更新肯定快。

# 9、Store

Store 是 Solid 用于处理嵌套响应式。只有单层的时候,使用Signal即可。

// @ts-nocheck
import { createSignal, Accessor, Setter } from 'solid-js';
import { createStore } from 'solid-js/store';
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

export default function InnerSignal() {
  let input: HTMLInputElement;
  let todoId = 0;

  const [store, setStore] = createStore<{ todos: Todo[] }>({ todos: [] });
  const handleAdd = () => {
    if (!input.value.trim()) return;
    // setTodos([...todos(), { id: todoId++, text: input.value, completed, setCompleted }]);
    setStore('todos', todos => [...todos, { id: ++todoId, text: input.value, completed: false }]);
    input.value = '';
  };

  const handleChange = id => {
    setStore(
      'todos',
      t => t.id === id,
      'completed',
      completed => !completed
    );
  };
  return (
    <div>
      <input type='text' ref={input} />
      <button onClick={handleAdd}>add todo</button>
      <ul>
        <For each={store.todos}>
          {(todo: Todo) => {
            console.log(todo.id);
            return (
              <li>
                <input type='checkbox' value={todo.completed} onChange={[handleChange, todo.id]} />
                <span style={{ 'text-decoration': todo.completed ? 'line-through' : 'none' }}>
                  {todo.text}
                </span>
              </li>
            );
          }}
        </For>
      </ul>
    </div>
  );
}
function Demo1() {
  const [person, setPerson] = createStore({
    name: {
      type: 'string',
      value: '222'
    },
    age: {
      type: 'number',
      value: 10
    }
  });
  const handleClick = () => {
    // 最基础的 setter 函数用法会接收一个对象,该对象的属性将与当前状态合并
    setPerson({
      name: {
        value: '333'
      }
    });
    // or
    // setter 函数还支持路径语法,以便我们进行嵌套更新
    // setPerson('name', 'value', '1111');
  };
  return (
    <div>
      <p>
        nameValue: {person.name.value}, ageValue: {person.age.value}
      </p>
      <button onClick={handleClick}>change</button>
    </div>
  );
}

# 10 配合product 修改Store

function Demo2() {
  type Item = { text: string };
  const [store, setStore] = createStore<{ list: Item[] }>({
    list: []
  });
  let code = 65;
  const add = () => {
    setStore(
      'list',
      produce(list => {
        list.push({ text: String.fromCharCode(code++) });
      })
    );
  };
  const change = () => {
    setStore(
      'list',
      1,
      produce(item => {
        item.text = 'B';
      })
    );
  };
  const handleDelete = (index: number) => {
    console.log(index);
    setStore(
      'list',
      produce(list => {
        list.splice(index, 1);
      })
    );
  };
  return (
    <>
      <hr />
      <ol>
        <li>
          <button onClick={add}>add</button>
          <button onClick={change}>change</button>
        </li>
        <Index each={store.list}>
          {(item: Accessor<Item>, index) => {
            return (
              <li>
                {item().text}
                <button onClick={[handleDelete, index]}>del</button>
              </li>
            );
          }}
        </Index>
      </ol>
      <hr />
    </>
  );
}

# 11、Context

相当于是全局上下文

import { createSignal, createContext, useContext, mergeProps, Accessor } from 'solid-js';
type Theme = 'dark' | 'light';
type ThemeSignal = [Accessor<Theme>, (a: Theme) => void];

// 提供 全局上下文内容 =======start=====
const ThemeContext = createContext<ThemeSignal>();

function ThemeProvider(props: { theme: Theme; children: any }) {
  const mProps = mergeProps({ theme: 'light' }, props);
  const [theme, setTheme] = createSignal<Theme>(mProps.theme);
  const store: ThemeSignal = [
    theme,
    (a: Theme) => {
      setTheme(a);
    }
  ];
  return <ThemeContext.Provider value={store}>{mProps.children}</ThemeContext.Provider>;
}

// 导出 useTheme 给子组件使用
export function useTheme() {
  return useContext(ThemeContext);
}
// =======end=====

function Child() {
  const [theme, changeTheme] = useTheme() as ThemeSignal;
  const change = (theme: Theme) => {
    changeTheme(theme);
  };
  return (
    <div>
      <p>{theme()}</p>
      <h1>child</h1>
      <button onclick={[change, 'dark']}>dark</button>
      <button onclick={[change, 'light']}>light</button>
    </div>
  );
}
function Child2() {
  const [theme, changeTheme] = useTheme() as ThemeSignal;

  return (
    <div>
      <p>{theme()}</p>
      <h1>child2</h1>
    </div>
  );
}

export default function ContextX() {
  return (
    <ThemeProvider theme='light'>
      <div></div>
      <Child></Child>
      <hr />
      <Child2></Child2>
    </ThemeProvider>
  );
}

# 12、通过createRoot使用全局的Signal

可以用于替代Context,都可以使用, 效果是一样的。

import { createMemo, createRoot, createSignal } from 'solid-js';

interface Cache {
  firstName?: string;
  lastName?: string;
  age?: number;
}

function createCache() {
  const [cache, setCache] = createSignal<Cache>({
    firstName: 'Lebern',
    lastName: 'James',
    age: 20
  });
  const changeCache = (newCache: Cache) => {
    setCache(cache => {
      return {
        ...cache,
        ...newCache
      };
    });
  };
  const fullName = createMemo(() => cache().firstName + ' ' + cache().lastName);
  return {
    cache,
    changeCache,
    fullName
  };
}

const cacheIns = createRoot(createCache);

function Demo1() {
  const { cache, changeCache, fullName } = cacheIns;
  const handleClick = () => {
    changeCache({ firstName: 'xxxx' });
  };
  return (
    <div>
      <h1>demo1</h1>
      <p>{cache().age}</p>
      <p>{fullName()}</p>
      <button onClick={handleClick}>change</button>
    </div>
  );
}

function Demo2() {
  const { cache, changeCache, fullName } = cacheIns;
  return (
    <div>
      <h1>demo2</h1>
      <p>{cache().age}</p>
      <p>{fullName()}</p>
    </div>
  );
}

export default function ContextNo() {
  return (
    <div>
      <Demo1></Demo1>
      <hr />
      <Demo2></Demo2>
    </div>
  );
}

# 13、batch 手动批处理更新

批处理 点击后, 函数C只会被执行一次

import { batch, createSignal } from 'solid-js';
export default function SimpleDemo() {
  const [A, setA] = createSignal('A');
  const [B, setB] = createSignal('B');
  const C = () => {
    console.log('rebuild C');
    return A() + ':' + B();
  };

  const handleC = () => {
    batch(() => {
      setA(A() + 'n');
      setB(B() + '!');
    });
  };
  return (
    <div>
      <p>C: {C()}</p>
      <button onClick={handleC}>change</button>
    </div>
  );
}

# 14、untrack 解除依赖被收集

// @ts-nocheck
import { createEffect, createSignal, untrack } from 'solid-js';
export default function UntrackX() {
  const [A, setA] = createSignal(1);
  const [B, setB] = createSignal(1);
  createEffect(() => {
    // untrack之后B的更新不会被副作用收集
    console.log(A(), untrack(B));
  });
  createEffect(() => {
    console.log(B());
  });
  const addA = () => {
    setA(A() + 1);
  };
  const addB = () => {
    setB(B() + 1);
  };
  return (
    <div>
      <p>
        A: {A()}, B: {B()}
      </p>
      <button onClick={addA}>increment A</button>
      <button onClick={addB}>increment B</button>
    </div>
  );
}

# 15、响应性 on

指定哪些依赖被收集。

设置显式依赖。这主要用来更明确地简洁地声明跟踪哪些信号。然而,它也允许计算不立即执行而只在第一次更改时运行。可以使用defer 选项启用此功能。

import { createEffect, createSignal, untrack, on } from 'solid-js';
export default function UntrackX() {
  const [A, setA] = createSignal(1);
  const [B, setB] = createSignal(1);

  createEffect(
    on(
      A,
      a => {
        console.log(a, B());
      },
      { defer: true }
    )
  );
  const addA = () => {
    setA(A() + 1);
  };
  const addB = () => {
    setB(B() + 1);
  };
  return (
    <div>
      <p>
        A: {A()}, B: {B()}
      </p>
      <button onClick={addA}>increment A</button>
      <button onClick={addB}>increment B</button>
    </div>
  );
}

# 16、lazy懒加载

const Comp1 = lazy(() => import('~/components/Comp1'));

# 17、

上次更新: 1/22/2025, 9:39:13 AM