pngwn / svelte-adapter

Use Svelte components with Vue and React
301 stars 10 forks source link

Support for SSR Frameworks #7

Open Gennnji opened 4 years ago

Gennnji commented 4 years ago

It will be very useful to make Svelte-components for use in SSR Frameworks such as React-based Next.js and Vue-based Nuxt.js. Maybe, after implementation an adapter for Angular, there will be a usefulness in adapter for Angular Universal.

Pagan-Idel commented 4 years ago

Does the svelte-adapter work in Next.js? My next.js app can't recognize my svelte component. Cannot find module error.

pngwn commented 4 years ago

I’m not 100% on SSR, svelte has separate SSR builds so that will need to be handled somewhere. I’ll take a closer look at this shortly.

Gennnji commented 4 years ago

Actually I realized adapter for Next.js for my work. If I'll have a time, I'll try to contribute.

pngwn commented 4 years ago

@Gennnji I'd love to take a look at your approach if possible. There are a few ways around this, that I can think of, but I have some reservations about which way makes the most sense.

Gennnji commented 4 years ago

@pngwn Sorry for such late answer. Here is how I adapted your code for Next.js:

const React = require('react');
const createReactClass = require('create-react-class');

// useRef somehow errors with Next.js (maybe I couldn't handle it rightfully)
// So functional component became class component and also a CommonJS module
module.exports = function(Component, ComponentSsr, style = {}, tag = 'span') {
  const ComponentAdapter = createReactClass({
    getInitialState: function() {
      this.container = React.createRef();
      this.component = React.createRef();

      const { head, html, css } = ComponentSsr && ComponentSsr.render
        ? ComponentSsr.render(this.props)
        : { head: '', html: '', css: '' };

      this.componentHtml = (css && css.code ? `<style>${css.code}</style>` : '') + html;

      return {};
    },

    componentDidMount: function() {
      if (!Component) {
        return;
      }

      const eventRe = /on([A-Z]{1,}[a-zA-Z]*)/;
      const watchRe = /watch([A-Z]{1,}[a-zA-Z]*)/;

      this.component.current = new Component({
        target: this.container.current,
        hydrate: true,
        props: this.props,
      });

      let watchers = [];
      for (const key in this.props) {
        const eventMatch = key.match(eventRe);
        const watchMatch = key.match(watchRe);

        if (eventMatch && typeof this.props[key] === 'function') {
          this.component.current.$on(
            `${eventMatch[1][0].toLowerCase()}${eventMatch[1].slice(1)}`,
            this.props[key]
          );
        }

        if (watchMatch && typeof this.props[key] === 'function') {
          watchers.push([
            // name of Svelte component prop being watched
            `${watchMatch[1][0].toLowerCase()}${watchMatch[1].slice(1)}`,
            // callback to run when Svelte component prop changes
            this.props[key]
          ]);
        }
      }

      if (watchers.length) {
        const update = this.component.current.$$.update;
        // Changed function to arrow function, so context wouldn't change
        this.component.current.$$.update = () => {
          watchers.forEach(([name, callback]) => {
            // Starting from some version of Svelte names and values of props
            // are in different places
            const index = this.component.current.$$.props[name];
            callback(this.component.current.$$.ctx[index]);
          });
          update.apply(null, arguments);
        };
      }
    },

    componentDidUpdate: function() {
      if (this.component.current) {
        this.component.current.$set(this.props);
      }
    },

    componentWillUnmount: function() {
      if (this.component.current) {
        this.component.current.$destroy();
      }
    },

    render: function() {
      return React.createElement(tag, {
        ref: this.container,
        style,
        dangerouslySetInnerHTML: {
          __html: this.componentHtml,
        },
      });
    }
  });

  return function(props) {
    return React.createElement(ComponentAdapter, props);
  }
}

For using in Next.js I generate two bundles for Svelte App: client-side and SSR. Also I use CommonJS module so Next.js could run SSR without errors (maybe I just haven't configure webpack rightfully). Oh, and I use UMD bundles for Svelte App, both for client-side and SSR, so Next.js could get identical html renders for both sides. One more, among other updates I made fix for searching watchers for last versions of Svelte. Here are lines of code:

            // Starting from some version of Svelte names and values of props
            // are in different places
            const index = this.component.current.$$.props[name];
            callback(this.component.current.$$.ctx[index]);
Gennnji commented 4 years ago

@pngwn Yeah, just found concrete version of Svelte where changes for $$.props and $$.ctx were made to use bitmask for change tracking - https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md#3160 - Svelte v3.16 Commit - https://github.com/sveltejs/svelte/pull/3945