Repository: apache/superset
File: superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx (TypeScript)

Code before:
    basicColorFormatters,
    basicColorColumnFormatters,
    hasServerPageLengthChanged,
    serverPageLength,
    slice_id,
  } = props;

  const comparisonColumns = useMemo(
    () => [
      { key: 'all', label: t('Display all') },
      { key: '#', label: '#' },
      { key: '△', label: '△' },
      { key: '%', label: '%' },
    ],
    [],
  );

  const timestampFormatter = useCallback(
    (value: DataRecordValue) => {
      if (value == null) {
        return '';
      }
      // In Raw Records mode, don't apply time grain-based formatting
      if (isRawRecords || !timeGrain) {
        return String(value);
      }
      return getTimeFormatterForGranularity(timeGrain)(
        value as number | Date | null | undefined,
      );
    },
    [timeGrain, isRawRecords],
  );
  const [tableSize, setTableSize] = useState<TableSize>({
    width: 0,
    height: 0,
  });
  // keep track of whether column order changed, so that column widths can too
  const [columnOrderToggle, setColumnOrderToggle] = useState(false);
  const [showComparisonDropdown, setShowComparisonDropdown] = useState(false);
  const [selectedComparisonColumns, setSelectedComparisonColumns] = useState([
    comparisonColumns[0].key,
  ]);
  const [hideComparisonKeys, setHideComparisonKeys] = useState<string[]>([]);
  // recalculated totals to display when the search filter is applied (client-side pagination)
  const [displayedTotals, setDisplayedTotals] = useState<D | undefined>(totals);
  const theme = useTheme();

  useEffect(() => {
    setDisplayedTotals(totals);
  }, [totals]);


Review comment (suggestion):
String(null) returns 'null', so it might be better to avoid producing the literal string 'null'. 
We could handle this case explicitly.

Code after:
    basicColorColumnFormatters,
    hasServerPageLengthChanged,
    serverPageLength,
    slice_id,
  } = props;

  const comparisonColumns = useMemo(
    () => [
      { key: 'all', label: t('Display all') },
      { key: '#', label: '#' },
      { key: '△', label: '△' },
      { key: '%', label: '%' },
    ],
    [],
  );

  const timestampFormatter = useCallback(
    (value: DataRecordValue) =>
      isRawRecords
        ? String(value ?? '')
        : getTimeFormatterForGranularity(timeGrain)(
            value as number | Date | null | undefined,
          ),
    [timeGrain, isRawRecords],
  );
  const [tableSize, setTableSize] = useState<TableSize>({
    width: 0,
    height: 0,
  });
  // keep track of whether column order changed, so that column widths can too
  const [columnOrderToggle, setColumnOrderToggle] = useState(false);
  const [showComparisonDropdown, setShowComparisonDropdown] = useState(false);
  const [selectedComparisonColumns, setSelectedComparisonColumns] = useState([
    comparisonColumns[0].key,
  ]);
  const [hideComparisonKeys, setHideComparisonKeys] = useState<string[]>([]);
  // recalculated totals to display when the search filter is applied (client-side pagination)
  const [displayedTotals, setDisplayedTotals] = useState<D | undefined>(totals);
  const theme = useTheme();

  useEffect(() => {
    setDisplayedTotals(totals);
  }, [totals]);

  // only take relevant page size options
  const pageSizeOptions = useMemo(() => {
    const getServerPagination = (n: number) => n <= rowCount;
    return (
      serverPagination ? SERVER_PAGE_SIZE_OPTIONS : PAGE_SIZE_OPTIONS
    ).filter(([n]) =>
      serverPagination ? getServerPagination(n) : n <= 2 * data.length,