0

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;
Meowster
  • 447
  • 3
  • 14

1 Answers1

1

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

snnsnn
  • 10,486
  • 4
  • 39
  • 44
  • Hi, thanks for your reply. My problem is precisely the usage of SolidJS components inside `formatter` of Grid.js – Meowster Apr 12 '23 at 13:50
  • My example is simplified. I really need to use SolidJS component inside the `formatter`, not just `h` function – Meowster Apr 12 '23 at 14:39
  • This solution isn't functional. It only copies inner text of a component. It doesn't mount the component into DOM. Also it incorrectly passes context of outer `render`. For example `` component of [Solid Router](https://github.com/solidjs/solid-router) used inside a `formatter` doesn't see routes – Meowster Apr 13 '23 at 08:43
  • 1
    @Meowster Do you understand the problem you are trying to solve. Gridjs uses VDOM much like react and formatter expect you to return an object, which will be used to create a DOM element, and there is no way to get underlying dom node to mount a solid component. Only way out is using innerText via html function, or `dangerouslySetInnerHTML` via `h` function, both of which accepts text only values to render. That is where we lose the reactivity. Component mounts but reactivity is not preserved because Solid needs an actual DOM node to properly function. – snnsnn Apr 13 '23 at 09:41
  • @Meowster continued.. Transitioning between VDOM to DOM without proper API is problematic. That is what I have been trying to tell you. What you are asking is doable, but won't be clean and performant. – snnsnn Apr 13 '23 at 09:42
  • > Also it incorrectly passes context of outer render. I don't understand your question. How can you move between DOM -> VDOM -> DOM again without render functions? Even if you don't use render function, any glue code will be re-creation of the render function anyway. – snnsnn Apr 13 '23 at 09:44
  • Thank You! I missed that Grid.js is based on Preact. [My solution](https://stackblitz.com/edit/solidjs-templates-i9pbn1?file=src/GridWrapper.ts). I'm using `getOwner` and `runWithOwner` to preserve context – Meowster Apr 13 '23 at 13:21
  • @Meowster You've got it right, i have been looking for that create ref fn, but Gridjs docs is terrible, never found it. One last thing, gridjs can easily be implemented in solid, if performance suffers don't hesitate to ditch it. – snnsnn Apr 13 '23 at 13:59