table icon indicating copy to clipboard operation
table copied to clipboard

[v8] Global filter gives `TypeError: l.toLowerCase is not a function` when using number fields

Open marceloverdijk opened this issue 2 years ago • 19 comments

Describe the bug

With latest 8.5.11 version using the Global filter in combination with number fields gives:

image

Note this was already raised in https://github.com/TanStack/table/issues/4210 but still happens with latest version unfortunately.

I think this was introduced somewhere in 8.3.x line as I first encountered this while upgrading from 8.2.x to 8.3.3.

A workaround like

      {
        id: 'matches',
        header: 'Matches',
        accessorFn: (originalRow) => originalRow.matches.toString(), // matches is a number
        // accessorKey: 'matches', // this worked before
      },

seems to work for the filtering, but this would also impact the sorting I would assume. Sorting on a string value instead of the number value will have impact (1, 2, 10 vs 1, 10, 2).

Your minimal, reproducible example

.

Steps to reproduce

See above.

Expected behavior

Global filter to work with number fields out of the box.

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

macOS, chrome

react-table version

v8.5.11

TypeScript version

4.7.4

Additional context

If reproducible example is needed let me know.

Terms & Code of Conduct

  • [X] I agree to follow this project's Code of Conduct
  • [X] I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.

marceloverdijk avatar Aug 05 '22 20:08 marceloverdijk

Experiencing the same issue in v8.5.11.

The following is shown in Console:

The above error occurred in the component
... stack trace
React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.
declare module "@tanstack/table-core" {
  interface FilterFns {
    testFilter: FilterFn<unknown>;
  }
}

function testFalsey(val: any) {
  return val === undefined || val === null || val === "";
}
  const [globalFilter, setGlobalFilter] = useState("");

  const testFilter: FilterFn<any> = (
    row,
    columnId: string,
    filterValue: unknown,
  ) => {
    console.log(columnId, filterValue);
    return true;
  };

  testFilter.resolveFilterValue = (val: any) => {
    console.log("resolveFilterValue", val);
    return `${val}`;
  };

  testFilter.autoRemove = (val: any) => {
    console.log("resolveFilterValue", val);
    return testFalsey(`${val}`);
  };

  const table = useReactTable({
    data,
    columns,
    state: {
      globalFilter,
    },
    filterFns: {
      testFilter,
    },
    onGlobalFilterChange: setGlobalFilter,
    // globalFilterFn: testFilter,           <-- custom filter is only triggered as globalFilterFn
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
  });

Things I've tried:

  • All built-in filter functions. -> results in above error
  • Add custom filter to filterFns, and set above custom filterFn. -> results in above error (filterFn not even triggered)
  • Set custom filterFn as globalFilterFn. -> triggered and not crashing
  • Set enableGlobalFilter: false -> obviously no error

dimbslmh avatar Aug 09 '22 16:08 dimbslmh

I am so novice on this. I copied the includesString built-in function and converted the value to string. And now use that as the globalFilterFn. Do you think it could be useful?


function testFalsey (val: any): boolean {
  return val === undefined || val === null || val === ''
}

export const includesString: FilterFn<any> = (
  row,
  columnId: string,
  filterValue: string
) => {
  const search = filterValue.toLowerCase()
  // Convert to String
  const value = String(row.getValue<string>(columnId))
  return value?.toLowerCase().includes(search)
}

includesString.autoRemove = (val: any) => testFalsey(val)```

LautaroRiveiro avatar Aug 12 '22 01:08 LautaroRiveiro

As a temporary fix I added

enableGlobalFilter: false,

To my number column

decayedCell avatar Aug 18 '22 13:08 decayedCell

@decayedCell unfortunately that is not really a fix when having a global filter in the first place 😉

marceloverdijk avatar Aug 18 '22 13:08 marceloverdijk

@decayedCell unfortunately that is not really a fix when having a global filter in the first place 😉

Not sure what you mean by this. I have ran into the same issue, I have 2 tables and both use global filter and a search component that sets the global filter. For the table that only has strings as fields there are no issues, but on the other one I ran into the same issue mentioned by you. So I disabled GlobalFilter for only that column that uses numbers and now I have no issues using my search component. Obviously the negative is that that number column is completely ignored now, but until a fix it makes the table not crash at least while still providing the global filter functionality for the other columns.

decayedCell avatar Aug 18 '22 13:08 decayedCell

Exactly what I meant, the column is completely ignored and that's not what I want

marceloverdijk avatar Aug 18 '22 14:08 marceloverdijk

@marceloverdijk You can keep the data types intact and just pass in sanitized tableData before you initialize the table like this:

  useEffect(() => {
    setTableData(
      data.map((row: any) => {
        Object.keys(row).forEach((column: any) => {
          if (row[column]) {
            row[column] = row[column].toString();
          } else {
            row[column] = '';
          }
        });
        return row;
      })
    );
  }, [data]);

It's not as pretty as just passing in data to useReactTable but it will solve your problems.

jeg924 avatar Aug 22 '22 22:08 jeg924

@marceloverdijk while this doesn't solve whatever the underlying issue in the code is... you can follow one of the examples to implement a fuzzy filter that replaces the global and seems to work fine.

https://codesandbox.io/s/github/tanstack/table/tree/main/examples/react/filters?from-embed=&file=/src/main.tsx

scoussens avatar Sep 01 '22 21:09 scoussens

I took a page from @LautaroRiveiro suggestion by overriding the globalFilterFn, but instead of casting all values as a String I do a check on whether the field is a number, and if so only then make a conversion to a string:

const globalFilterFn: FilterFn<T> = (row, columnId, filterValue: string) => {
  const search = filterValue.toLowerCase();

  let value = row.getValue(columnId) as string;
  if (typeof value === 'number') value = String(value);

  return value?.toLowerCase().includes(search);
};

oldo avatar Sep 13 '22 04:09 oldo

I think the reason why this is happening is because of the following commit: a5578ac5670b6c8739ce9e12f8045a17dbcd8823

The default behavior of getColumnCanGlobalFilter will now return true for any columns that are either string or number, but the default filter functions don't have the same flexibility. So the various options for working around the bug are as follows:

  1. Change all your columns to string type
  2. Use your own filter function which allows either string or number type
  3. Override getColumnCanGlobalFilter in your table options so that it goes back to old behavior of only returning true for string type columns

I also hit this problem from a different direction - I wanted to filter on string array type columns - which is possible to do if you have a flexible filter function that handles array types - but that column would never appear because getColumnCanGlobalFilter filtered it out.

I think there are three things that could happen going forward, assuming the goal is that global filters should be type-flexible in the default case.

  1. Provide a convenient factory function that can create a getColumnCanGlobalFilter function based on supported types (i.e. we should be able to specify which types we want to allow for a particular table without having to override the entire function supplied in default filter options)
  2. There should be a type-flexible version of includesString which can at least handle all of the types allowed by getColumnCanGlobalFilter, or even better handle any type thrown at it by stringifying it
  3. The documentation says that if getColumnCanGlobalFilter is not supplied, all columns are assumed to be globally filterable (https://tanstack.com/table/v8/docs/api/features/filters#can-filter). But that doesn't appear to be the case, because the default options do filter out certain columns (https://github.com/TanStack/table/blob/main/packages/table-core/src/features/Filters.ts#L174). So perhaps the documentation should be updated too.

I also noticed if you don't supply a globalFilterFn it defaults to "auto", which I guess is supposed to figure out the correct filter to use depending on the column type and avoid the user having to do any of this complicated setup themselves, but in my case that's not working either, because it decides to use includesString for a number column.

alisonatwork avatar Nov 01 '22 07:11 alisonatwork

The same issue. I use the latest version "@tanstack/react-table": "^8.5.22";

I am getting TypeError: o.toLowerCase is not a function when I use the globalFilter feature which is basic one and is not fuzzy implementation.

There are one number-type data column, two string-type data columns and a number of display columns in my Table definition. (There is no group header)

If I remove the sole number-type data column, the table does not render at all having the same error. If I keep the number-type data column, I receive the error when I type the input field which is debounced input related with globalFilter as it is in the examples in the tanstack web site.

talatkuyuk avatar Nov 06 '22 10:11 talatkuyuk

I get the same "o.toLowerCase is not a function" error when I have a number-type column, even on latest v8.5.27 released today.

I solved it by simply adding .toString() on the number column's accessorFn.

While the solution is quite simple, it was not easy to understand and I hope it'll be fixed soon, or at least have better error handling so developers will have a better idea of what to do in this situation.

OmerWow avatar Nov 14 '22 08:11 OmerWow

Experiencing the same issue. Solved it thanks to @oldo's suggestion.

const table = useReactTable({
      data,
      columns,
      globalFilterFn: (row, columnId, filterValue) => {
        const safeValue = (() => {
          const value = row.getValue(columnId);
          return typeof value === 'number' ? String(value) : value;
        })();

        return safeValue?.toLowerCase().includes(filterValue.toLowerCase());
      }
      ...
    });

codebycarlos avatar Dec 08 '22 17:12 codebycarlos

@oldo's solution did the trick for me. But, I was just wondering if there's an actual difference when validating the type of value this way:

if (!isNaN(parseInt(value))) value = String(value);

The thing is that the conditional in the solution gives:

"Invalid 'typeof' check: 'value' cannot have type 'number'"

stupeyca avatar Feb 24 '23 00:02 stupeyca

works fine when I used "includesString"?

ssarahs-lab avatar Mar 02 '23 09:03 ssarahs-lab

When you define your columns and you are going to filter specific data from a column make sure to add the filterFn property:

filterFn: (row, id, value) => {
    return value.includes(row.getValue(id))
}

soymartinez avatar Jun 26 '23 20:06 soymartinez

When you define your columns and you are going to filter specific data from a column make sure to add the filterFn property:

filterFn: (row, id, value) => {
    return value.includes(row.getValue(id))
}

Thats worked for me! Thanks

hadnazzar avatar Aug 01 '23 17:08 hadnazzar

Alternative way to think about it

I was working with date and numeric columns The accessorFn used would return number or dates or undefined depending on the column. This would then make the native sorting functionality to work

The column's cell function would then format the numbers or dates for rendering

I noticed that the globalFilterFn was ignoring these specific fields when I debugged my code To solve this, I had to

  1. format the date and amount values to string as the return value of the accessorFn
  2. Create custom sortingFn for the respective columns and access the raw value from Row data

Below is an example of one column definition

const columnHelper = createColumnHelper<Order>();

const DateSortFn =
  (accessor: (row: Order) => Date | undefined) =>
  (rowA: Row<Order>, rowB: Row<Order>, columnId: any): number => {
    const rowAValue = accessor(rowA.original) || new Date(0);
    const rowBValue = accessor(rowB.original) || new Date(0);

    return rowAValue < rowBValue ? 1 : -1;
  };

const validUntilColumnDef = columnHelper.accessor(
      (row) => formatDate(row.quoteValidTo, "MMM d, yyyy"),
      {
        header: getColumnHeaderName("valid-until"),
        sortingFn: DateSortFn((row) => row.quoteValidTo),
        id: "valid-until",
        cell: HighlightableCell,
      }
    )
    

elijahkimani avatar Oct 14 '23 16:10 elijahkimani

filterFn: (row, id, value) => {
    return value.includes(row.getValue(id))
}

Shouldn't this be the other way round? At least for me, to filter a number column the following works:

filterFn: (row, id, value: string) => {
  const rowValue = row.getValue<string>(id).toString();
  return rowValue.includes(value);
},

eikowagenknecht avatar Feb 15 '24 16:02 eikowagenknecht