solidjs / solid

A declarative, efficient, and flexible JavaScript library for building user interfaces.
https://solidjs.com
MIT License
32.1k stars 914 forks source link

Reactive properties not updating in solid-element custom elements #2278

Closed telephant closed 1 week ago

telephant commented 1 week ago

Describe the bug

I've encountered an issue with solid-element where reactive properties are not updating correctly when the Props change.

Your Example Website or App

none

Steps to Reproduce the Bug or Issue

  1. Create a custom element using solid-element
  2. Import in React project
  3. Pass properties via React elements

Expected behavior

Solid components can receive props correctly and reactively

Screenshots or Videos

customElement<{ roomId: string, type: 'push' | 'player' }>(
  'cst-live',
  { roomId: '1', type: 'push' },
  LiveMain,
);

LiveMain is a solid component, can not receive the roomId correctly.

Below is the React project which import solid web componets


  roomId={roomId}
  type={amIAnchor ? 'push' : 'player'}
/>```

### Platform

- OS: [e.g. macOS, Windows, Linux]
- Browser: [e.g. Chrome, Safari, Firefox]
- Version: [e.g. 91.1]

### Additional context

_No response_
telephant commented 1 week ago

The data on dom is correctly update, but solid components can't receive it.

And if I update roomId the dom in devtools, the solid components will not update as well

ryansolid commented 1 week ago

We would greatly benefit from a reproduction for this. Something a little more tangible than import in a React project.

telephant commented 1 week ago

Hi, thanks for your reply.😊

I tried the vue project and offcial demo, encountering the same issue. It doesn't seem to be a 'React thing'.

I just update the demo project mentioned in official README.md document. I added property to pass the room argument to the web component. The default value of room is 'ccc', and the Stroybook passed argument value is 'eeee'. However the dom rendered the default value 'ccc' instead of 'eeee'. Even when I tried updating the room property directly through devtools. It didn't have any effect.

Reproduce: https://studio.webcomponents.dev/edit/WPJFBXuhVhYMD4lEYtNr/src/index.stories.js?p=README.md

ryansolid commented 1 week ago

As far as I can tell the example works. The storybook version gets the updated eeee and the Readme gets the default because a value isn't set there. Upon setting it in the Readme.md I see the updated value.

telephant commented 1 week ago

As far as I can tell the example works. The storybook version gets the updated eeee and the Readme gets the default because a value isn't set there. Upon setting it in the Readme.md I see the updated value.

Oh! Yes, you are right. I made a mistake of this online editor, it renders README.md by default. So it works for me now.

But in my project I still can not get the props correctly.

Solid component:


import {
  getCurrentElement,
} from 'solid-element';
import { customElement } from 'solid-element';
import {
  onMount,
  onCleanup,
  createSignal,
  type Component,
} from 'solid-js';
import { extractCss } from "solid-styled-components";
import tailwindStyles from '../../../assets/styles/tailwind.css?inline';
import varStyles from '../../../assets/styles/var.css?inline';
import LivePush from '../push';

const sheet = new CSSStyleSheet();
sheet.replaceSync(tailwindStyles);

interface LiveMainProps {
  roomId: string;
  type: 'push' | 'player';
}

const LiveMain: Component<LiveMainProps> = (props) => {
  const {
    roomId,
    type,
  } = props;

  const [_roomId, setRoomId] = createSignal(roomId);

  onMount(() => {
    const ele = getCurrentElement();
    if (ele.shadowRoot) {
      ele.shadowRoot.adoptedStyleSheets = [sheet];
      const cssText = extractCss();

      const styleVar = document.createElement('style');
      styleVar.textContent = varStyles;
      ele.shadowRoot.appendChild(styleVar);

      const style = document.createElement('style');
      style.textContent = cssText;
      ele.shadowRoot.appendChild(style);
    }

    // 初始设置 roomId
    setRoomId(ele.getAttribute('roomId') || props.roomId);

    // 创建一个 MutationObserver 来监听属性变化
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === 'attributes' && mutation.attributeName === 'roomId') {
          const newRoomId = ele.getAttribute('roomId');
          setRoomId(newRoomId || props.roomId);
          console.log('RoomId updated:', newRoomId);
        }
      });
    });

    observer.observe(ele, { attributes: true });

    // 清理函数
    onCleanup(() => {
      observer.disconnect();
    });
  });

  return (
    <div>
      <p class="text-4xl text-red-600 test222">
        {roomId}
      </p>
      <p class="text-4xl text-red-600 test222">
        {_roomId()}
      </p>
      {type === 'push' && (
        <LivePush
          roomId={_roomId()}
        />
      )}
    </div>
  );
};

customElement<{ roomId: string, type: 'push' | 'player' }>(
  'cst-live',
  { roomId: '1', type: 'push' },
  LiveMain,
);

vite.config:

import * as path from 'path';
import { fileURLToPath } from 'url';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import solidPlugin from 'vite-plugin-solid';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const __packageDir = path.resolve(__dirname, '../');

export default defineConfig({
  plugins: [
    dts({
      tsconfigPath: './tsconfig.json',
      rollupTypes: true,
    }),
    solidPlugin(),
  ],
  resolve: {
    alias: {
      '@cst-live-sdk': path.resolve(__packageDir, 'cst-live-sdk/src/index.ts'),
      '@cst-request-encryption': path.resolve(__packageDir, 'cst-request-encryption/src/index.ts'),
      '@cst-media-device': path.resolve(__packageDir, 'cst-media-device/src/index.ts'),
      '@common-library': path.resolve(__packageDir, 'common-library/src/index.ts'),
      '@cst-web-rtc-sdk': path.resolve(__packageDir, 'cst-web-rtc-sdk/src/index.ts'),
    },
  },
  css: {
    modules: {
      localsConvention: 'dashesOnly',
    },
  },
  build: {
    outDir: 'dist',
    target: 'esnext',
    emptyOutDir: true,
    lib: {
      entry: path.resolve(__dirname, 'src/index.ts'),
      name: 'CSTLiveSolutionSDK',
      fileName: (format) => {
        return `${format}/index.js`;
      },
    },
    rollupOptions: {
      external: [],
      output: {
        globals: {
          // 'solid-js': 'SolidJS',
          // 'solid-element': 'SolidElement',
        },
      },
    },
  },
});

package.json:

{
  "name": "cst-live-solution-sdk",
  "version": "0.0.1",
  "description": "",
  "main": "dist/index.js.mjs",
  "types": "dist/index.d.ts",
  "scripts": {
    "start": "vite",
    "dev": "vite build --watch --mode development",
    "build": "vite build",
    "serve": "vite preview"
  },
  "license": "MIT",
  "devDependencies": {
    "autoprefixer": "^10.4.20",
    "postcss": "^8.4.40",
    "solid-devtools": "^0.29.2",
    "tailwindcss": "^3.4.7",
    "typescript": "5.4.2",
    "vite": "^5.0.11",
    "vite-plugin-solid": "^2.8.2"
  },
  "dependencies": {
     ...,
    "solid-element": "^1.8.1",
    "solid-js": "^1.8.11",
    "solid-styled-components": "^0.28.5",
    "tslib": "^2.5.0",
    "typescript": "^5.4.0",
    "video.js": "^8.17.4",
    "vite-plugin-dts": "^4.1.0"
  }
}
telephant commented 1 week ago

I can get properties correctly from const ele = getCurrentElement(); ele. getAttribute('roomId');

ryansolid commented 1 week ago

If I had to guess it is because you are destructuring props and losing reactivity. So while it may get the initial value it won't get any updated one.

telephant commented 1 week ago

Thank you for the suggestion, I tried to remove the props destructuring but it doesn't work. Here is the code:


import {
  customElement,
  getCurrentElement,
} from 'solid-element';
import {
  onMount,
  createSignal,
  type Component,
} from 'solid-js';
import { extractCss } from "solid-styled-components";
import tailwindStyles from '../../../assets/styles/tailwind.css?inline';
import varStyles from '../../../assets/styles/var.css?inline';
import LivePush from '../push';

const sheet = new CSSStyleSheet();
sheet.replaceSync(tailwindStyles);

interface LiveMainProps {
  roomId: string;
  type: 'push' | 'player';
}

export const LiveMain: Component<LiveMainProps> = (props) => {
  const [_roomId] = createSignal(props.roomId);

  onMount(() => {
    const ele = getCurrentElement();
    if (ele.shadowRoot) {
      ele.shadowRoot.adoptedStyleSheets = [sheet];
      const cssText = extractCss();

      const styleVar = document.createElement('style');
      styleVar.textContent = varStyles;
      ele.shadowRoot.appendChild(styleVar);

      const style = document.createElement('style');
      style.textContent = cssText;
      ele.shadowRoot.appendChild(style);
    }
  });

  return (
    <div>
      <p class="text-4xl text-red-600 test222">
        {props.roomId}
      </p>
      <p class="text-4xl text-red-600 test222">
        {_roomId()}
      </p>
      {props.type === 'push' && (
        <LivePush
          roomId={_roomId()}
        />
      )}
    </div>
  );
};

const data: { roomId: string, type: 'push' | 'player' } = { roomId: 'test', type: 'push' };
customElement<{ roomId: string, type: 'push' | 'player' }>(
  'cst-live',
  data,
  (props) => {
    console.log('🚀 ===== props:', props); // still get default value: { roomId: 'test', type: 'push' }
    return LiveMain(props);
  },
);

Should I add some extra plugin for vite to support the web component in solidjs?

ryansolid commented 1 week ago

No only standard Solid plugin is needed. There is still a misunderstanding in the code. Solid only runs components once. Removing the signal altogether and using props.room directly in the JSX should alleviate some of this. Also can't see inside LivePush but I'd be looking for similar issues.

As simple debug try console.log props.roomId inside createEffect

telephant commented 1 week ago

OMG,I sorted it out. Web components can process the camel case which I used in my project. 🥺 😭

Thank you so much for your help!!!!!!!!!!! And this is my first SolidJs project, it's awesome! Do you have any recommendations for headless components in solidJS or web components

ryansolid commented 1 week ago

Oh right.. let me see where it is documented. But it's possible there's some camel case dash case conversion going on. Sorry I wrote the core library here a decade ago and I don't use it often anymore so I'm a bit foggy.

Yeah.. if you set your props as camelCase it makes them as dash cased attributes by default. So roomId would be room-id as an attribute (but still roomId as a property on the element).

You can override it with a prop definition that sets what you want the attribute name to be. I need to document this. But passing an object with value and attribute will do that:

customElement<{ roomId: string, type: 'push' | 'player' }>(
  'cst-live',
  { roomId: {
    value: '1',
    attribute: "roomid" // force it to just be lowercase version
  }, type: 'push' },
  LiveMain,
);
telephant commented 1 week ago

Oh right.. let me see where it is documented. But it's possible there's some camel case dash case conversion going on. Sorry I wrote the core library here a decade ago and I don't use it often anymore so I'm a bit foggy.

Yeah.. if you set your props as camelCase it makes them as dash cased attributes by default. So roomId would be room-id as an attribute (but still roomId as a property on the element).

You can override it with a prop definition that sets what you want the attribute name to be. I need to document this. But passing an object with value and attribute will do that:

customElement<{ roomId: string, type: 'push' | 'player' }>(
  'cst-live',
  { roomId: {
    value: '1',
    attribute: "roomid" // force it to just be lowercase version
  }, type: 'push' },
  LiveMain,
);

Yesss!