Skip to content

Conversation

evnchn
Copy link
Collaborator

@evnchn evnchn commented May 3, 2025

This PR fix #2744 with a rather heavy-handed approach of converting all data types which could possibly cause Vue to grind to a halt to be string, and disables dicts altogether.

{982BB1D4-AA24-4854-842D-E2BC015F882F}

This brings the behaviour in line to Vue

<html>
  <head>
    <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet" type="text/css" />
    <link href="https://cdn.jsdelivr.net/npm/quasar@2.15.1/dist/quasar.prod.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <div id="q-app">
      <q-table :columns="[{'name': 'A','label': 'A','field': 'A'}]" :rows="[{'A': 'string'}, {'A': 123}, {'A': true}, {'A': new Date('2025-05-03')}, {'A': [1, 2, 3]}, {'A': { key: 'value' }}, {'A': null}, {'A': function() { return true; }}, {'A': Symbol('a')}]" row-key="id" />
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@2.15.1/dist/quasar.umd.prod.js"></script>
    <script>
      const app = Vue.createApp({ setup() { return {}; } });
      app.use(Quasar);
      app.mount("#q-app");
    </script>
  </body>
</html>

@evnchn evnchn added bug Type/scope: A problem that needs fixing 🌿 intermediate Difficulty: Requires moderate understanding labels May 3, 2025
@falkoschindler
Copy link
Contributor

Thanks for the initiative, @evnchn! The problem with freezing browsers due to lists in table cells is bugging us for a while now. But I have some concerns:

  1. If I understand correctly, this PR prevents the client from entering an infinite recursion when given problematic cell values. But converting to string somehow hides the fact that the values are invalid in the first place. What does the user expect to see when creating a table cells containing lists?

    ui.table(rows=[{'A': [1, 2, 3]}])
  2. I'm still not sure why this problem only occurs for NiceGUI apps. Plain Quasar apps don't freeze in this situation. If we'd find out what make the difference, we might be able to fix it and rely on Quasar to do the string conversion itself.

  3. You're calling this PR "a rather heavy-handed approach". I agree: We're adding computational load for everyone, just to mitigate the effect of invalid cell values.

  4. By creating a new list instance in _filter_rows you're breaking user code which assumes table.rows is still the same instance like the initializer argument. When updating its values and calling table.update(), this PR won't reflect the update in the UI.

@falkoschindler falkoschindler added the analysis Status: Requires team/community input label May 3, 2025
@evnchn
Copy link
Collaborator Author

evnchn commented May 3, 2025

Thank you @falkoschindler on the performance and code compatability insights. Personally I am quite hesitant on this PR since it does seem very heavy-handed.

But I will say the, old API is also way too lenient. I would expect row: List[Dict[int | str]] or something like that, not rows: List[Dict]

Point 1: What happens on passing [1, 2, 3], as in ui.table(rows=[{'A': [1, 2, 3]}])

The behaviour is by-design to match what Vue does.

Check the 5th row of the image:

image

They will get "123", because that's what happens if there are multiple text nodes as well.

{A285FDAE-E966-4859-9155-9B6DAE5CB575}

Though there are multiple text nodes, it is equivalent in terms of HTML. In fact, if you click to try edit, this happens:

{C31ECC28-C81F-49EF-813D-9D3593022287}

This indicates that, indeed, [1,2,3] should be considered as "123"

Point 2: Why only NiceGUI affected

Something may be wrong with the mounting process, which your MRE does not capture as there is no component and there is no mounting to be done.

Check out the following patch to table.js

  beforeCreate() {
    console.log('Attributes before create:', JSON.stringify(this.$attrs.rows));
  },
  beforeMount() {
    console.log('Attributes before mount:', JSON.stringify(this.$attrs.rows));
  },
  mounted() {
    console.log('Attributes:', JSON.stringify(this.$attrs.rows));
  },

And note the console output:

Attributes before create: [{"A":["B",1,"One"]}]
Attributes before mount: [{"A":["B",1,"One"]}]
Attributes: [{"A":[{"__v_isVNode":true,"__v_skip":true,"props":null,"key":null,"ref":null,"scopeId":null,"slotScopeIds":null,"children":"B","target":null,"targetStart":null,"targetAnchor":null,"staticCount":0,"shapeFlag":8,"patchFlag":0,"dynamicProps":null,"dynamicChildren":null,"appContext":null,"dirs":null,"transition":null,"component":null,"suspense":null,"ssContent":null,"ssFallback":null,"el":{},"anchor":null,"ctx":null},{"__v_isVNode":true,"__v_skip":true,"props":null,"key":null,"ref":null,"scopeId":null,"slotScopeIds":null,"children":"1","target":null,"targetStart":null,"targetAnchor":null,"staticCount":0,"shapeFlag":8,"patchFlag":0,"dynamicProps":null,"dynamicChildren":null,"appContext":null,"dirs":null,"transition":null,"component":null,"suspense":null,"ssContent":null,"ssFallback":null,"el":{},"anchor":null,"ctx":null},{"__v_isVNode":true,"__v_skip":true,"props":null,"key":null,"ref":null,"scopeId":null,"slotScopeIds":null,"children":"One","target":null,"targetStart":null,"targetAnchor":null,"staticCount":0,"shapeFlag":8,"patchFlag":0,"dynamicProps":null,"dynamicChildren":null,"appContext":null,"dirs":null,"transition":null,"component":null,"suspense":null,"ssContent":null,"ssFallback":null,"el":{},"anchor":null,"ctx":null}]}]

So something in the mounting process has turned my strings and integers in the list into VNodess.

This may be an interesting thing to look into.

Point 3: Actually I agree this is quite heavy-handed. But I just don't want the code to hang the browser on prod. Perhaps we can do with a warning instead of the filtering process? What do you think?

Point 4: That's an oversight on my part. Oops.


Given the above, this PR isn't 100% perfect. More like 70%. Should we rather handle this issue with better documentation and perhaps a helper function to help users do the filtering?

@falkoschindler
Copy link
Contributor

Your analysis in #4694 (comment) is pretty interesting. I don't think concatenating 1, 2 and 3 into 123 does make much sense, but at least the browser isn't freezing. The different mounting process could be the key to an explanation. Maybe we can re-create the problem with a Vue example including components and mounting? If so, we should continue the discussion on the original issue #2744.

I still think the best way forward is to find out what is creating the recursion and try to stop it. Therefore I suggest to close this PR because I don't find it feasible to check every single cell just because some users might feed invalid data (shooting their own foot).

@evnchn
Copy link
Collaborator Author

evnchn commented May 7, 2025

I don't think concatenating 1, 2 and 3 into 123 does make much sense, but at least the browser isn't freezing.

But that could be intended behaviour from Quasar / Vue. I'd imagine it's like an implicit ''.join(...). So actually I am more inclined to keep the concatenation, removing the risk of infinite loop & making our table match the Quasar implementation.

Maybe we can re-create the problem with a Vue example including components and mounting?

Sorry but I am weak with regards to Vue. It's a black box to me. Perhaps you or Cursor can try it.

I still think the best way forward is to find out what is creating the recursion and try to stop it.

I may be pessimistic, but I don't think we could ever find a way to stop the recursion.

It is because, if this Reddit reply is anything to go by, "anything is reactive with the Options API", so it will always recursively update itself due to the reactivity.

https://www.reddit.com/r/vuejs/comments/15jiu52/comment/jv2ct4k/

Almost everything is reactive by default with Options API. You can’t mutate the props period with Options - or if you somehow find a way, it’s UB - while with composition having an object o, you can do ref(o) and whenever you change the ref you also change the original object (etc.)

And in fact, I recall I have experimented with passing a mutated copy of this.$attrs, which doesn't have VNodes in the rows. However, the moment the component was mounted, apparently Vue can't stop itself from screening through my copy of the customAttrs and re-adding VNodes for me.

Sorry but I did not have source-control active for that experiment, and I don't have the code anymore.

My opinion is, without turning off Vue's intervention which cannot be possible without #4673 and composition API, I don't see a way out.

At least, I don't think I could ever find a way to stop the recursion, with my limited understanding. @xaptronic Sorry for the mention, but you seem much more smarter than me in terms of how Vue works. Would you like to check this out?


However, the need to prevent browsers from grinding to a halt is a real one. I suggest the following approach, with the primary goal being to minimize the number of browsers hung in the meantime.

  • Document clearly, that passing a list as value is and always has been an undefined behaviour for NiceGUI as of now:
    • Pre NiceGUI 2.17.0: Causes the browser to freeze up (put this in red text)
    • NiceGUI 2.17.0: Some way which
    • Put clealy that "Behaviour is subject to change, so do not treat this as a stable API, join the strings yourself for maximum stability" in the warning displayed, and in the documentation.
  • Then, we can buy more time to investigate into this issue without fear of people's browser freezing.
  • If we then figure out a change, we can apply it, because we said very clearly the case of passing a list as value is an undefined behaviour, so we can do whatever we want with regards to changing it.

I also have an idea: Can the Vue component screen for VNodes in the rows and columns, and refuse to render and error out if so? This also prevents the infinite loop, but the server code can stay the same, automatically ensuring user code not broken in Python side.

@falkoschindler what do you think?

Still, this PR has to be a draft, since as it stands it breaks user code.

@evnchn evnchn marked this pull request as draft May 7, 2025 22:33
@xaptronic
Copy link
Contributor

xaptronic commented May 8, 2025

Plain quasar min reproducible example, which attempts to mimic what NiceGUI does - the difference between the "plain Quasar app" and NiceGUI is that, in NiceGUI, rows are passed in as props. It seems that when the row props are passed to the table, there is a mutation that occurs which updates the prop values themselves (because arrays are passed by reference) which causes the reactive nature of the props to consider themselves changed and causes infinite recursion.

<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link href="https://cdn.jsdelivr.net/npm/quasar@2.15.1/dist/quasar.prod.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <div id="q-app">
      <test-table :rows="tableRows" />
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@2.15.1/dist/quasar.umd.prod.js"></script>
    <script>
      const TestTable = {
        template: `
          <q-table
            :columns="columns"
            :rows="rows"
            row-key="id"
          />
        `,
        props: {
          rows: {
            type: Array,
            required: true,
          },
        },
        data() {
          return {
            columns: [
              {
                name: "A",
                label: "A",
                field: "A",
              },
            ],
          };
        },
      };

      const app = Vue.createApp({
        components: {
          TestTable,
        },
        data() {
          return {
            tableRows: [
              { A: "string" },
              { A: 123 },
              { A: true },
              { A: new Date("2025-05-03") },
              { A: [1, 2, 3] },
              { A: { key: "value" } },
              { A: null },
              {
                A: function () {
                  return true;
                },
              },
              { A: Symbol("a") },
            ],
          };
        },
      });
      app.use(Quasar);
      app.mount("#q-app");
    </script>
  </body>
</html>

To work around this, one way is to "clone" the rows as a computed property - here is a crude example using JSON.parse/JSON.stringify

<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link href="https://cdn.jsdelivr.net/npm/quasar@2.15.1/dist/quasar.prod.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <div id="q-app">
      <test-table :rows="tableRows" />
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@2.15.1/dist/quasar.umd.prod.js"></script>
    <script>
      const TestTable = {
        template: `
          <q-table
            :columns="columns"
            :rows="clonedRows"
            row-key="id"
          />
        `,
        props: {
          rows: {
            type: Array,
            required: true,
          },
        },
        data() {
          return {
            columns: [
              {
                name: "A",
                label: "A",
                field: "A",
              },
            ],
          };
        },
        computed: {
          clonedRows() {
            /* inflate/deflate in order to send something to q-table that isn't memory referenced to the prop to avoid reactivity updates due to mutations */
            return JSON.parse(JSON.stringify(this.rows));
          },
        },
      };

      const app = Vue.createApp({
        components: {
          TestTable,
        },
        data() {
          return {
            tableRows: [
              { A: "string" },
              { A: 123 },
              { A: true },
              { A: new Date("2025-05-03") },
              { A: [1, 2, 3] },
              { A: { key: "value" } },
              { A: null },
              {
                A: function () {
                  return true;
                },
              },
              { A: Symbol("a") },
            ],
          };
        },
      });
      app.use(Quasar);
      app.mount("#q-app");
    </script>
  </body>
</html>

I didn't look as far as why the prop values are being mutated.

mounting process has turned my strings and integers in the list into VNodes

regarding this comment, the first thing I tried was to use explicit :rows prop instead of using depending on v-bind - I found that the row props was also converted to v-nodes prior to render the template, so I'm not sure if that is the culprit.

I don't think this is really related to composition vs options API, and imo, I don't think introducing behaviour to modify the data being sent through to avoid this bug in the python/NiceGUI layer is quite the right way to solve this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

analysis Status: Requires team/community input bug Type/scope: A problem that needs fixing 🌿 intermediate Difficulty: Requires moderate understanding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Browser can't load page when table rows contain lists

3 participants