从React项目 迁移到 Solid项目的踩坑记录

从React项目 迁移到 Solid项目的踩坑记录

solid 中父子组件执行次序和 react 不一样

注意,这里的 子组件是指 children形式的子组件。

对于非children形式的子组件,都是先执行父组件在执行子组件。

tsx 复制代码
const ComponentA = ({ children }) => {
    console.log('ComponentA')
    return (
        <div style={{ background: 'skyblue' }}>
          {/* children形式的子组件 */}
            {children}
        </div>
    );
};

对比一般形式的子组件写法:

tsx 复制代码
/* 父组件 */
const ComponentA = ({  }) => {
  console.log('ComponentA')
  return (
    <div style={{ background: 'skyblue' }}>
      /* 子组件 */
      <ChildComp/>
    </div>
  );
};

在 React 中,先执行父组件,再执行children子组件

tsx 复制代码
import { Test } from './components/test/index';

function App() {
  console.log('App')

  return (
    <div>
      <Test></Test>
    </div>
  );
}

export default App
tsx 复制代码
import React from "react";

const ComponentA = ({ children }) => {
  console.log('ComponentA')
  return (
    <div style={{ background: 'skyblue' }}>
      {children}
    </div>
  );
};


const ComponentB = () => {
  console.log('ComponentB')
  return (
    <div style={{ background: 'pink' }}>
      ComponentB
    </div>
  );
};

const ComponentC = () => {
  console.log('ComponentC')
  return (
    <div style={{ background: 'red' }}>
      ComponentC
    </div>
  );
};


export const Test = () => {
  console.log('test')
  return (
    <div>
      <ComponentA>
        <ComponentC></ComponentC>
      </ComponentA>

      <ComponentB></ComponentB>
    </div>
  )
}

是先执行 父组件函数后才执行子组件函数,即使是 children形式的子组件。

在 Solid 中,先执行children子组件,再执行父组件

tsx 复制代码
import { Component, ParentComponent } from 'solid-js';

const ComponentA: ParentComponent = ({ children }) => {
  console.log('ComponentA')
  return (
    <div style={{ background: 'skyblue' }}>
      {children}
    </div>
  );
};



const ComponentB = () => {
  console.log('ComponentB')
  return (
    <div style={{ background: 'pink' }}>
    </div>
  );
};

const ComponentC = () => {
  console.log('ComponentC')
  return (
    <div style={{ background: 'red' }}>
      ComponentC
    </div>
  );
};


export const Test = () => {
  console.log('test')
  return (
    <div>
      <ComponentA>
        <ComponentC></ComponentC>
      </ComponentA>

      <ComponentB></ComponentB>
    </div>
  )
}

这导致的迁移问题

tsx 复制代码
import DynamicRender from 'xxx'
import { createCtx } from 'xxx'

const [useGlobalCtx, GlobalProvider] = createCtx(useCreateGlobalCtx)

const App = () => {
  console.log('APP')
  return (
    <div id="app">
      <GlobalProvider>
        <DynamicRender />
      </GlobalProvider>
    </div>
  );
};
tsx 复制代码
import { createContext, ParentComponent, useContext } from "solid-js"

export const createCtx = <T extends Record<string, any> | null>(
  hook: () => T
) => {

  const Ctx = createContext<T | undefined>(undefined)

  function useCtx() {
    // debugger
    const c = useContext(Ctx)
    if (c === undefined)
      throw new Error('获取 context 必须包裹在Provider里 并提供value')
    return c
  }

  const Provider:ParentComponent = ({ children }) => {
    // 2. 父组件函数后执行
    console.log('Ctx.Provider')
    const value = hook()

    // 1. children 子组件先执行
    return <Ctx.Provider value={value}>{children}</Ctx.Provider>
  }

  return [useCtx, Provider] as const
}

注意上面这种写法的生命周期,console.log('Ctx.Provider')最后打印的,也就是说 Ctx.Provider value={value}是最后才有 value 值的。

问题:先调用子组件children函数,才调用父组件函数,当 子组件children函数被调用时,里面使用了useContext去获取上下文的 value值,由于 value 值是在父组件函数中提供的,但是父组件还没有执行,这就导致子组件获取不到 value值,导致报错。

解决

不使用封装的父组件去包裹上下文的Provider( Ctx.Provider),避免 children形式的子组件写法:

tsx 复制代码
import DynamicRender from '@/components/dynamic-render'
import { GlobalProvider } from './hooks/useGlobalCtx'
import "./app.less"

const Ctx = createContext<T | undefined>(undefined)

const App = () => {
  console.log('APP')

  const value = {}

  return (
    <div id="app">
      <Ctx.Provider value={value}>
        <DynamicRender />
      </Ctx.Provider>Ctx.Provider>
    </div>
  );
};

solid 组件更新机制 和 react 不同

对于 React

在 React 中,父组件中的状态变化时,父组件更新,子组件默认更新,因为 react 不知道父组件中的状态是否有被子组件使用到。

对于 solid

在 solid 中,当信号量变化时,只有该信号量的订阅者会响应数据的变化。也就是说,如果父组件中定义了一个信号量,这个 信号量如果在通过 props 传递给子组件时不是信号类型,那么子组件中就没办法使用该信号量,这样的情况下,当父组件中的信号量更新时,子组件不会更新。

可能会存在的问题

我们封装一个组件,可能需要根据外部传入的 props 来 更新组件的状态(受控组件),对于 solid 来说,由于其细粒度响应式的更新机制, 我们要实现上述的效果,那么父组件传递给子组件的 prop 就必须得是Accessor类型。

如果一个组件的 props 中大部分属性都需要变为Accessor,那我们在定义 ts 类型时就需要多次用Accessor<>包裹原数据类型,很麻烦,如:

tsx 复制代码
interface Props {
  expired: Accessor<boolean>
  privilegeNumber: Accessor<number>
  assetNumber: Accessor<number>
  showTooltip?: Accessor<boolean>
  mid?: Accessor<number>
  callback1: () => {}
  callback2: () => {}
}

解决

我们可以实现一个 ts 类型工具:

tsx 复制代码
import { Accessor } from "solid-js";

/** 泛型工具类型,将 Props 中的每个属性值包装进 Accessor 类型 */
// export type AccessorizeProps<Props> = {
// 	[K in keyof Props]: Accessor<Props[K]>;
// };


/** 泛型工具类型,将 Props 中的每个属性值包装进 Accessor 类型,除了那些在 ExcludeKeys 中指定的键 */
export type AccessorizeProps<Props, ExcludeKeys extends keyof Props = never> = {
  [K in keyof Props as K extends ExcludeKeys ? never : K]: Accessor<Props[K]>;
} & {
  [K in keyof Props as K extends ExcludeKeys ? K : never]: Props[K];
}

关键部分解释:K in keyof Props as K extends ExcludeKeys ? never : K

  • 作用 : 遍历 Props 的所有键,并根据 ExcludeKeys 过滤掉指定的键。
  • 过程:
    1. Props 的每个键 K 进行遍历。
    2. 检查 K 是否存在于 ExcludeKeys 中:
      • 如果存在,则将其映射为 never,从而从结果类型中移除。
      • 如果不存在,则保留该键 K

使用:

tsx 复制代码
interface Props {
  expired: boolean
  privilegeNumber: number
  assetNumber: number
  showTooltip?: boolean
  mid?: number
  callback1: () => {}
  callback2: () => {}
}

type NewProps = AccessorizeProps<
  Props,
  "callback1" | "callback2"
  >

图片 src 处 require() 不能使用模版字符串

项目相关包版本如下:

json 复制代码
"vite": "^5.2.0",
  "vite-plugin-html": "^3.2.2",
  "vite-plugin-require-transform": "^1.0.21",
  "vite-plugin-solid": "^2.11.0"
"solid-js": "^1.9.3",

src 处 不能使用模版字符串

tsx 复制代码
<img src={require(`@assets/images/ic_experience.png`)} alt="" />

只能使用普通字符串

tsx 复制代码
<img src={require('@assets/images/ic_experience.png')} alt="" />

否则报错如下:

javascript 复制代码
Failed to resolve import "" from "xxxx/aaaaa.tsx". Does the file exist?
  import * as _vite_plugin_require_transform__0 from "";

jotai 状态管理库迁移

Jotai 是一个原始且灵活的 React 状态管理库。

由于 solid 的信号可以在组件外创建,所以我们可以直接使用 全局信号来当做全局状态。

使用 jotai:

tsx 复制代码
import { atom } from 'jotai'

export const secondFloorShowAtom = atom<boolean>(false)

直接使用信号量:

tsx 复制代码
import { createSignal } from 'solid-js'

const [secondFloorShow, setSecondFloorShow] = createSignal<boolean>(false)

export {
  secondFloorShow,
  setSecondFloorShow
}

完~

欢迎补充,欢迎指正~

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰8 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy9 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom10 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom10 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试