SolidJS Typescript 开发指南(6) Props和Store

Props

Props指的是某个函数式组件传入的参数,在使用组件之前,SolidJS已经将一系列数据封装到函数式组件的形参中了,在这里要介绍的是SolidJS对Props给出的各种API

1. mergeProps

在大部分情况下,我们想要封装一个自定义组件,或者是一个工具方法,都要考虑默认参数

interface FnType {
	id: string,
	name?: string,
}
function fn(options: FnType) {
	// 这里name属性是一个不确定内容,如果要做成默认参数的效果,需要写成这样
	console.log(`id: ${optoins.id}, name: ${optoins.name || "default name"}`);
}

SolidJS提供了一个API以合并两个参数,同时保证它的响应式

import type { Component } from "solid-js";
import { mergeProps } from "solid-js";
interface GreetingProps {
    greeting?: string;
    name?: string;
}
export const Greeting: Component<GreetingProps> = function (props) {
    const merged = mergeProps({ greeting: "Hi", name: "John" }, props);
    return (
        <h3>
            {merged.greeting} {merged.name}
        </h3>
    );
};

2. splitProps

既然有合并props,对应的也有拆分props的API

export const Greeting: Component<GreetingProps> = function (props) {
    const [local, others] = mergeProps(props, ["name", "greeting"]);
    return (
        <h3>
            {local.greeting} {local.name}
        </h3>
    );
};

总结

之所以使用以上两个API来操作Props,是因为传入的Props对象在底层依赖对象的 getter 来获取值,如果使用普通的对象合并或者是对象解构会导致对象最终失去响应性,对应dom也不会更新。

在Props中我们会发现,父组件改变了传递的值之后,子组件并不会触发onMounted钩子函数,这是为了最小化更新Dom中的节点,所有更新决策全部交给SolidJS管理。
那么我们在生命周期中该如何发现更改的数值以及做出相应的操作呢,答案就是创建一个由SolidJS创建的跟踪范围,即createEffect

import type { Component } from "solid-js";
import { splitProps, onMount, createEffect } from "solid-js";
interface GreetingProps {
    greeting?: string;
    name?: string;
}
export const Greeting: Component<GreetingProps> = function (props) {
    const [local, others] = splitProps(props, ["name", "greeting"]);
    // 这里只执行一次
    console.log("greeting init", local.name);
    onMount(() => {
        // 这里只执行一次
        console.log("greeting mounted");
    });
    // 这里每次更新name都会触发回调函数的执行
    createEffect(() => console.log("name =", local.name));
    return (
        <h3>
            {local.greeting} {local.name}
        </h3>
    );
};

Store

SolidJS可以进行嵌套更新的原因之一是它实现了细粒度的响应式。这种响应式允许SolidJS可以只更新关联数据中最细粒度的dom,而不是更新整个关联数据对应的dom。Store可以看作是进阶版的Signal,它可以更轻易地处理细粒度更新的易操作性

举个例子(来自官网的举例):想象一个SolidJS构成的Todo List,每一个Todo Item都是一个响应式对象,而Todo List是一个由CreateSignal创建的有序数组。
每个Item都包含以下三个数据:Id:number、text:string和completed:boolean

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

要求每次点击Item就要切换这一项数据中的completed

const [todos, setTodos] = createSignal<Array<Todo>>([]);
let input: any;
let todoId = 0;

const addTodo = (text: string) => {
    setTodos([...todos(), { id: ++todoId, text, completed: false }]);
};
const toggleTodo = (id: number) => {
    setTodos(todos().map((todo) => (todo.id !== id ? todo : { ...todo, completed: !todo.completed })));
};

现在问题出现了,由于SolidJS的单向流动型,Todo List只能通过createSignal创建出的set方法更新,这样我们更新的就是完整的List,这样需要SolidJS去对比整个列表的差异然后再进行最小化更新,怎么去优化这个流程呢?

官网给出的方法是把每一个Item的completed改成响应式数据,把get方法和set方法全部存入List的每一项中,然后点击时再根据id找到对应的index调用它的set方法,这样就只更新了其中一项的状态而不是整个列表

import type { Component, Accessor, Setter } from "solid-js";
// ...
interface Todo {
    id: number;
    text: string;
    completed: Accessor<boolean>;
    setCompleted: Setter<boolean>;
}
// ...
const [todos, setTodos] = createSignal<Array<Todo>>([]);
let input: any;
let todoId = 0;

const addTodo = (text: string) => {
    const [completed, setCompleted] = createSignal<boolean>(false);
    setTodos([...todos(), { id: ++todoId, text, completed, setCompleted }]);
};
const toggleTodo = (id: number) => {
    const todo = todos().find((t) => t.id === id);
    if (todo) todo.setCompleted(!todo.completed());
};
// ...

为了解决这种操作的复杂度,SolidJS提供了Store去操作更细粒度的数据

import { createStore } from "solid-js/store";
// ...
const [todos, setTodos] = createStore<Array<Todo>>([]);
// 在这里todos变量不需要加()就可以使用,因此在For的参数each直接使用<For each={todos}>
let input: any;
let todoId = 0;

const addTodo = (text: string) => {
    setTodos([...todos, { id: ++todoId, text, completed: false }]);
};
const toggleTodo = (id: number) => {
    setTodos(
        (todo) => todo.id === id,
        "completed",
        (completed) => !completed
    );
};
// ...

get方法差别不是很大,只是在使用时不需要加括号了,所以接下来更加详细地介绍一下createStore创建的set方法
这里依然使用官网的举例

const [state, setState] = createStore<{ firstName: string; middleName?: string; lastName: string }>({
    firstName: "John",
    lastName: "Miller",
});
// 可以直接修改state
setState({ firstName: "Johnny", middleName: "Lee" });
// 或者
setState("firstName", "Johnny");

// 如果要获取原先的数据,则可以传入一个回调函数
setState((state) => ({ middleName: state.firstName, lastName: "Milner" }));
// 或者
setState("firstName", (prev) => prev + "ny");

现在返回到Todo List的举例,如果我们需要对有序数组进行操作

// 传入回调函数
setTodos(list => [...list, {id: ++todoId, text: "new Todo", completed: false}]);
// 传入下标然后获取该项内容进行修改
setTodos(2, "completed", true);
// 传入数组的范围然后更改其包含的所有项
setTodos([0, 2], "completed", true);
// 传入更完整的内容修改内容,如果填空对象则表示不进行筛选
setTodos({ from: 0, to: 1, by: n }, "completed", (c) => !c); // 这里by是隔n项设置一次
// 筛选还可以通过传入函数
setTodos((todo) => todo.completed, "text", (t) => t + "!");

这样的语法优化能覆盖除了 超过三层深度的树结构 大部分的使用场景

// 最多筛选三层深度结构,更深层次会报错: 类型实例化过深,且可能无限。ts(2589)
 setTree(
    (item) => !!item.children, // 第一层 tree.children
    "children",
    (item) => !!item.children, // 第二层 tree.children.children
    "children",
    (item) => !!item.children, // 第三层 tree.children.children.children
    (item) => item // setter
);

produce

还记得store的set函数中参数setter吗,在进行筛选后使用setter可以修改store的数据,以进行响应式更新,但这个setter只接受同步函数,如果使用异步函数就会报错(在ts里),SolidJS提供了类似生产者的角色produce充当setter

setTodos(
    produce(async (list) => {
        const text: string = await getNew();
        // 异步生产数据提供给store
        list.push({ id: ++todoId, text, completed: false });
    })
);

edit: 更新context的使用说明

context

context是SolidJS提供的一个全局变量访问入口,使用了依赖注入的思想,使用Provider包裹在组件外层以提供对全局变量的访问,子组件在使用useProvider时会向上查找,具体例子在官网上也是一目了然,这里介绍一下ts下的写法

import type { Component, JSX, Accessor } from "solid-js";
import { createSignal, createContext, useContext } from "solid-js";
// 指定返回内容的类型,这样ts解构对象时不会报错
type contextTools = {
    increment: () => void;
    decrement: () => void;
};
type contextProviderType = [Accessor<number>, contextTools];
const CounterContext = createContext<contextProviderType>();

export const CounterProvider: Component<{ children?: JSX.Element; count?: number }> = function (props) {
    const [count, setCount] = createSignal<number>(props.count || 0);
    // 这里需要直接指定store类型,不然其类型推断和我们需要的返回值不一样
    const store: contextProviderType = [
        count,
        {
            increment() {
                setCount((c) => c + 1);
            },
            decrement() {
                setCount((c) => c - 1);
            },
        },
    ];
    return <CounterContext.Provider value={store}>{props.children}</CounterContext.Provider>;
};
export function useCounter(): contextProviderType {
    /**
     * solidjs提供的createContext在发生错误的情况下会返回undefined
     * 而ts在解构对象时不允许出现可能存在undefined的情况
     * 因此这里需要把undefined的情况直接抛错,在最外层进行容错处理
     */
    const context = useContext(CounterContext);
    if (context) {
        return context;
    } else {
        throw new Error("create context failed");
    }
}
// 在组件内使用全局变量
import { useCounter } from "../global/counter";
export default function ContextInner() {
    const [count, { increment, decrement }] = useCounter();
    return (
        <>
            <div>{count()}</div>
            <button onClick={increment}>+</button>
            <button onClick={decrement}>-</button>
        </>
    );
}

// 在外层组件进行包裹
import type { Component } from "solid-js";
import ContextInner from "../components/ContextInner";
import { CounterProvider } from "../global/counter";

const Index: Component = () => {
    return (
        <CounterProvider count={1}>
            <h1>Welcome to Counter App</h1>
            <ContextInner />
        </CounterProvider>
    );
};

export default Index;

总结

store是SolidJS在signal的基础上的另一个响应式API,它允许更细粒度的操作以达到最小化diff以节省性能,在这一点上确实比React灵活度更高。同时store也可以和Immutable对象进行代理交互,感兴趣的同学们可以先了解一下Immutable的概念,React的状态容器Redux就是作为一个加载Immutable对象的管理插件在React中起到不可替代的作用

关于store对不可变数据的支持,可以在官网体验与redux嵌合的各种操作。
Context API允许组件之间不通过prop传递Signal或其他响应式变量,而Context依然属于SolidJS响应式系统的一部分,这对于非父子组件之间数据传递很有意义。

本文链接: https://blog.csdn.net/weixin_41907106/article/details/126385698

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值