Search code examples
solid-jsgridjs

Use SolidJS components within Grid.js


I'm trying to use a custom formatter feature of Grid.js with SolidJS component. Here is a sandbox on Stackblitz.

I am getting the following error:

computations created outside a `createRoot` or `render` will never be disposed

I tried to mimic an adapter for React, but I failed. Please help me understand how properly use SolidJS rendering system in this situation.

Code listing of sandboxed example:

import { Component, JSXElement, mergeProps } from 'solid-js';

import { Grid, createRef, h } from 'gridjs';
import 'gridjs/dist/theme/mermaid.css';
import { onMount } from 'solid-js';
import { render } from 'solid-js/web';

type Row = [id: number, name: string, age: number];

type Cells<T extends [...any[]]> = {
  [K in keyof T]: {
    data: T[K];
  };
};
type FormatterRow<T extends [...any[]]> = {
  cells: Cells<T>;
};

const NameCell: Component<{ id: Row[0]; name: Row[1] }> = (props) => {
  return <a href={`/user/${props.id}`}>{props.name}</a>;
};

const Wrapper: Component<{ element: JSXElement; parent?: string }> = (
  rawProps
) => {
  const props = mergeProps({ parent: 'div' }, rawProps);

  const ref = createRef();

  onMount(() => {
    render(() => props.element, ref.current);
  });

  return h(props.parent, { ref });
};

const wrap = (element: JSXElement, parent?: string) => {
  return h(Wrapper, { element, parent });
};

const Table: Component<{ rows: Row[] }> = (props) => {
  let gridRef!: HTMLDivElement;

  const grid = new Grid({
    columns: [
      { name: 'Id', hidden: true },
      {
        name: 'Name',
        formatter: (
          name: Row[1],
          { cells: [{ data: id }] }: FormatterRow<Row>
        ) => wrap(<NameCell id={id} name={name} />),
      },
      'Age',
    ],
    data: props.rows,
    sort: true,
    search: true,
  });

  onMount(() => {
    grid.render(gridRef);
  });

  return <div ref={gridRef} />;
};

const App: Component = () => {
  const rows: Row[] = [
    [1, 'Andrew', 14],
    [2, 'Mike', 45],
    [3, 'Elsa', 28],
  ];

  return <Table rows={rows} />;
};

export default App;

Solution

  • You get that error when you access a reactive data outside a tracking scope. Computations that are created outside a tracking scope can not be discarded which leads to memory leaks, that is why solid emits that error.

    You create a tracking scope by calling createRoot: https://www.solidjs.com/docs/latest/api#createroot

    But when you call render, an implicit tracking scope will be created:

    All Solid code should be wrapped in one of these top level as they ensure that all memory/computations are freed up. Normally you do not need to worry about this as createRoot is embedded into all render entry functions.

    Now, about your problem: You need to call the render function at the root of your app, but you are calling it later, inside the Wrapper via wrap variable. This messes up your component hierarchy.

    Solution is simple: Render your app first, then mount your Grid instance later and for that you don't need to call the Solid's render function, just take a reference to the element and render your grid instance:

    import { Grid } from 'gridjs';
    import { Component, onMount } from 'solid-js';
    import { render } from 'solid-js/web';
    
    type Row = [id: number, name: string, age: number];
    
    const Table: Component<{ rows: Array<Row> }> = (props) => {
      let gridRef!: HTMLDivElement;
    
      const grid = new Grid({
        columns: ['Order', 'Name', 'Age'],
        data: props.rows,
        sort: true,
        search: true,
      });
    
      onMount(() => {
        grid.render(gridRef);
      });
    
      return <div ref={gridRef} />;
    };
    
    export const App: Component<{}> = (props) => {
      const rows: Array<Row> = [
        [1, 'Andrew', 14],
        [2, 'Mike', 45],
        [3, 'Elsa', 28],
      ];
    
      return (
        <div>
          <Table rows={rows} />
        </div>
      );
    };
    
    render(() => <App />, document.body);
    

    PS: formatter requires you to return string or VDOM element using h function from the gridjs, although you can mount a solid component on top of, it is best to avoid it and use its own API:

    columns: [
      { id: 'order', name: 'Order' },
      {
        id: 'name',
        name: 'Name',
        formatter: (cell, row) => {
          return h(
            'a',
            {
              href: `/user/${cell}`,
              onClick: () => console.log(cell),
            },
            cell,
          );
        },
      },
      {
        id: 'age',
        name: 'Age',
      },
    ],
    

    If you really need to use Solid for formatter function, here is how you can do it:

    import { Grid, html } from 'gridjs';
    import { Component, onMount } from 'solid-js';
    import { render } from 'solid-js/web';
    
    const OrderFormatter: Component<{ order: number }> = (props) => {
      return <div>Order# {props.order}</div>;
    };
    
    type Row = [id: number, name: string, age: number];
    
    const Table: Component<{ rows: Array<Row> }> = (props) => {
      let gridRef!: HTMLDivElement;
      const grid = new Grid({
        columns: [
          {
            id: 'order',
            name: 'Order',
            formatter: (cell: number) => {
              let el = document.createElement('div');
              render(() => <OrderFormatter order={cell} />, el);
              return html(el.innerText);
            },
          },
          {
            id: 'name',
            name: 'Name',
          },
          {
            id: 'age',
            age: 'Age',
          },
        ],
        data: props.rows,
        search: true,
      });
    
      onMount(() => {
        grid.render(gridRef);
      });
    
      return <div ref={gridRef} />;
    };
    
    export const App: Component<{}> = (props) => {
      const rows: Array<Row> = [
        [1, 'Andrew', 14],
        [2, 'Mike', 45],
        [3, 'Elsa', 28],
      ];
    
      return (
        <div>
          <Table rows={rows} />
        </div>
      );
    };
    

    Gridjs uses VDOM and html does not render HTML elements but text, so we had to use some tricks:

    formatter: (cell: number) => {
      let el = document.createElement('div');
      render(() => <OrderFormatter order={cell} />, el);
      return html(el.innerText);
    }
    

    Ps: Turns out there is an API to access underlying VDOM element in Gridjs, so we can ditch the innerText and directly mount Solid component on the formatter leaf. runWithOwner is a nice touch to connect the isolated Solid contexts back to the parent context:

    import { JSXElement, runWithOwner } from 'solid-js';
    import {
      createRef as gridCreateRef,
      h,
      Component as GridComponent,
    } from 'gridjs';
    import 'gridjs/dist/theme/mermaid.css';
    import { render } from 'solid-js/web';
    
    export class Wrapper extends GridComponent<{
      element: any;
      owner: unknown;
      parent?: string;
    }> {
      static defaultProps = {
        parent: 'div',
      };
    
      ref = gridCreateRef();
    
      componentDidMount(): void {
        runWithOwner(this.props.owner, () => {
          render(() => this.props.element, this.ref.current);
        });
      }
    
      render() {
        return h(this.props.parent, { ref: this.ref });
      }
    }
    
    export const wrap = (
      element: () => JSXElement,
      owner: unknown,
      parent?: string
    ) => {
      return h(Wrapper, { element, owner, parent });
    };
    

    Check out the comments for details.

    Also check this answer for more details on createRoot function:

    SolidJS: "computations created outside a `createRoot` or `render` will never be disposed" messages in the console log