Openstatus www.openstatus.dev

chore: data-table blog post

authored by

Maximilian Kaske and committed by
Maximilian Kaske
166b4e1d b056f5a3

+211 -1
+1
apps/server/src/libs/cache/index.ts
··· 1 + export * from "./memory";
+51
apps/server/src/libs/cache/memory.ts
··· 1 + // Props to https://github.com/isaacs/node-lru-cache?tab=readme-ov-file#storage-bounds-safety 2 + 3 + export class MemoryCache { 4 + private ttl: number; 5 + private data: Map<string, unknown>; 6 + private timers: Map<string, NodeJS.Timeout>; 7 + 8 + constructor(defaultTTL = 60 * 1000) { 9 + this.ttl = defaultTTL; 10 + this.data = new Map(); 11 + this.timers = new Map(); 12 + } 13 + 14 + set<T>(key: string, value: T, ttl = this.ttl) { 15 + if (this.timers.has(key)) { 16 + clearTimeout(this.timers.get(key)); 17 + } 18 + this.timers.set( 19 + key, 20 + setTimeout(() => this.delete(key), ttl), 21 + ); 22 + this.data.set(key, value); 23 + return value; 24 + } 25 + 26 + get<T>(key: string) { 27 + return this.data.get(key) as T; 28 + } 29 + 30 + has(key: string) { 31 + return this.data.has(key); 32 + } 33 + 34 + delete(key: string) { 35 + if (this.timers.has(key)) { 36 + clearTimeout(this.timers.get(key)); 37 + } 38 + this.timers.delete(key); 39 + return this.data.delete(key); 40 + } 41 + 42 + clear() { 43 + this.data.clear(); 44 + for (const timer of this.timers.values()) { 45 + clearTimeout(timer); 46 + } 47 + this.timers.clear(); 48 + } 49 + } 50 + 51 + const cache = new MemoryCache();
apps/web/public/assets/posts/the-data-table-i-always-wanted/after.png

This is a binary file and will not be displayed.

apps/web/public/assets/posts/the-data-table-i-always-wanted/before.png

This is a binary file and will not be displayed.

apps/web/public/assets/posts/the-data-table-i-always-wanted/toolbox.png

This is a binary file and will not be displayed.

+1 -1
apps/web/src/components/content/mdx.tsx
··· 13 13 // FIXME: weird behaviour when `prose-headings:font-cal` and on mouse movement font gets bigger 14 14 <div 15 15 className={cn( 16 - "prose prose-slate dark:prose-invert prose-pre:my-0 prose-img:rounded-lg prose-pre:bg-background prose-pre:rounded-lg prose-img:border prose-pre:border prose-img:border-border prose-pre:border-border prose-headings:font-cal prose-headings:font-normal", 16 + "prose prose-slate dark:prose-invert prose-pre:my-0 prose-img:rounded-lg prose-pre:bg-background prose-pre:rounded-lg prose-img:border prose-pre:border prose-img:border-border prose-pre:border-border prose-headings:font-cal prose-headings:font-normal prose-blockquote:font-light prose-blockquote:border-l-2", 17 17 className, 18 18 )} 19 19 >
+158
apps/web/src/content/posts/the-data-table-i-always-wanted.mdx
··· 1 + --- 2 + title: The data-table I always wanted 3 + description: Better design, new features, and performance improvements. 4 + author: 5 + name: Maximilian Kaske 6 + url: https://x.com/mxkaske 7 + avatar: /assets/authors/max.png 8 + publishedAt: 2025-02-02 9 + image: /assets/posts/the-data-table-i-always-wanted/toolbox.png 10 + tag: engineering 11 + --- 12 + 13 + First, a note: While there's still a long way to go, the PR [#11](https://github.com/openstatusHQ/data-table-filters/pull/11) marks an important second milestone after the initial release a few months ago. We now have a solid foundation to focus on the component API design and data fetching for the data table. Though you can create your own data table using config files (for "sheet," "filters," and "columns"), you end up writing more code than you would like. 14 + 15 + If you want to try out the demo right away: [logs.run/i](http://logs.run/i) 16 + 17 + ### Design improvements 18 + 19 + We've reworked the design. Adding table borders improves clarity and structure. We've replaced the `Check` icon with the rounded square already used in the Chart to maintain design consistency. We've also removed the "green" highlighting to emphasize bad requests instead. 20 + 21 + A quick look at **before** (first screenshot) and **after** (second screenshot): 22 + 23 + ![before](/assets/posts/the-data-table-i-always-wanted/before.png) 24 + 25 + ![after](/assets/posts/the-data-table-i-always-wanted/after.png) 26 + 27 + We have been inspired by [Vercel](http://vercel.com/?ref=openstatus), [Datadog](http://datadoghq.com/?ref=openstatus) and [Axiom](http://axiom.co/?ref=openstatus) when it comes to design and features. 28 + 29 + ### Features 30 + 31 + This time, we prioritized keyboard navigation to make table access and navigation quickly. We've added: 32 + 33 + - "Skip to content" to jump straight to the table 34 + - "Tab" navigation (with potential Arrow key support coming) + "Enter" keypress on rows 35 + - Include filters in the details `<Sheet />` 36 + - Additional hotkeys to quickly: 37 + - `⌘ Esc` reset focus (to the body, starting with "Skip to content") 38 + - `⌘ .` reset filters 39 + - `⌘ U` undo column states like order or visibility 40 + 41 + The W3 WAI (Web Accessibility Initiative) has two concise patterns for grids: The [Grid Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) and the [Treegrid Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/). The Grid Pattern focuses on cell navigation via arrows and the Treegrid Pattern focuses on hierarchical data with sub-rows, also using arrows to navigate. We might want to take an even closer look to the Web Standards to respect them. 42 + 43 + The log data table leverages a lot of [tanstack table](https://tanstack.com/table/latest/docs/introduction) core features and adds some additional customization to it. 44 + 45 + Here's what's included in our logs data table: 46 + 47 + 1. column resizing 48 + 2. column reordering 49 + 3. column visibility 50 + 4. column sorting 51 + 5. custom filter functions 52 + 6. array facets support 53 + 7. custom row/cell/header styles via `meta` data 54 + 8. …and much much more 55 + 56 + **Creating data tables via configs** might seem like over-engineering, especially given how unique and edge-case-heavy they can be. However, this approach can simplify many common use cases and serve as a valuable reference for building data tables with tanstack table. 57 + 58 + ### Issues and hacks 59 + 60 + We've encountered numerous issues along the way (unfortunately, I missed some of them). Here are the notable ones: 61 + 62 + The biggest challenge was **browser compatibility** for `table` and `thead` HTML tags: making the `thead` sticky while horizontally scrollable with borders was surprisingly tricky. While it worked in Chrome, Safari wouldn't show the header border, despite using the same approach that worked for table cells. After some CSS exploration, we found a solution. Read about the issue [here](https://stackoverflow.com/questions/50361698/border-style-do-not-work-with-sticky-position-element/53559396#53559396). 63 + 64 + Several smaller hacks were necessary: 65 + 66 + To **highlight the table rows**, we added negative outlines to fit within the container. This prevents the table overflow from cutting off the outline. We couldn't use the border attribute since the table's left-hand border serves as a separator between filter controls and main content (including cmd k, chart, and toggles). 67 + 68 + Used tailwindcss classes: `focus-visible:outline outline-1 -outline-offset-1 outline-primary focus-visible:bg-muted/50 data-[state=selected]:outline` 69 + 70 + > In general, whenever there are outline issues due to `overflow-hidden`, I often tend to add negative margin with the same positive padding to the element `-m-* p-*`. 71 + 72 + To **reset the active focus element** (returning to the first focusable element in the document), we found no web standard solution. While `document.activeElement.blur()` dismisses the current focus, it remembers the last focused position. Our solution: manually setting and removing a `tabindex` attribute on the `body`. 73 + 74 + ```jsx 75 + document.body.setAttribute("tabindex", "0"); 76 + document.body.focus(); 77 + document.body.removeAttribute("tabindex"); 78 + ``` 79 + 80 + While not an issue in this update, we've repeatedly found that when using `recharts`, the **date property can't be a Date()** for x-axis label reading and formatting. You must use it as `string` (toString) or `number` (getTime) – not the most intuitive approach. 81 + 82 + One persistent issue is the flickering of default values on the `<Accordion defaultOpen={...} />;`. This occurs on the official [radix-ui](https://www.radix-ui.com/primitives/docs/components/accordion) during page refresh (not client-side navigation) in Safari/Firefox, but not Chrome. We have opened an issue [#12](https://github.com/openstatusHQ/data-table-filters/issues/12) if you have solved that problem before and want to contribute. 83 + 84 + > Whenever I encounter an issue, I try to leave a `REMINDER:` comment. That way, I can easily search within the files and have a good reminder to not remove the code untested. Also this helped me to write that blog section at the end without having to leave the code. 85 + 86 + ### Performance 87 + 88 + Performance is improving. We've moved most of the state into a dedicated context, so only components consuming it rerender. Previously, our entire `data-table-infinite.tsx` component would trigger rerenders for all child components on any property change. 89 + 90 + Very important: **we will stick with the [shadcn](https://ui.shadcn.com/) defaults** and avoid additional libraries except [`nuqs`](https://nuqs.47ng.com/), an excellent type-safe search params state manager supporting major React frameworks. We'll keep using React Context for state management, letting you choose your preferred library (zustand, jotai, redux) when needed. 91 + 92 + We've added `debounce` to all possible controls to prevent renders on every keystroke. This helps with input searches and slider value changes. 93 + 94 + A simple example of reducing the re-rendery is by using a dedicated `ControlsContext` that toggles the `data-expaneded` attribute. With css only, you can then hide or show containers based on the value. See the @taiwindcss v3/v4 example: 95 + 96 + ```tsx 97 + interface ControlsContextType { 98 + open: boolean; 99 + setOpen: React.Dispatch<React.SetStateAction<boolean>>; 100 + } 101 + 102 + export const ControlsContext = React.createContext<ControlsContextType | null>(null); 103 + 104 + export function ControlsProvider({ children }: { children: React.ReactNode }) { 105 + const [open, setOpen] = React.useState(true); 106 + 107 + return ( 108 + <ControlsContext.Provider value={{ open, setOpen }}> 109 + <div 110 + /** 111 + * How to use the controls state without rerendering the children 112 + * components that do not consume the context with tailwind: 113 + * "hidden group-data-[expanded=true]/controls:block" (v3/v4) 114 + * "hidden group-data-expanded/controls:block" (v4) 115 + */ 116 + className="group/controls" 117 + data-expanded={open} 118 + > 119 + {children} 120 + </div> 121 + </ControlsContext.Provider> 122 + ); 123 + } 124 + 125 + export function useControls() { 126 + const context = React.useContext(ControlsContext); 127 + 128 + if (!context) { 129 + throw new Error("useControls must be used within a ControlsProvider"); 130 + } 131 + 132 + return context as ControlsContextType; 133 + } 134 + ``` 135 + 136 + The new [React Compiler](https://react.dev/learn/react-compiler) reduces our need for memoization while delivering great out-of-the-box performance. We’ve enabled in our Nextjs project, and we plan to include it in our future Vite example. We still need to add virtualization for handling larger tables (rendering only visible portions of the list). 137 + 138 + If you want to learn when your components rerender, I highly recommend the [react-scan](https://github.com/mxkaske/react-scan) library. 139 + 140 + We can avoid one full table rerender on row selection, which happens due to the `rowSelection` key used for outlining selected rows while the `<Sheet />` is open. But hey, it's a nice visual touch to see which row is selected, so we're keeping it for now. 141 + 142 + ### Feature Requests 143 + 144 + The mobile navigation needs more love. Horizontal scrolling now gives access to previously hidden columns. Currently, we simply place filter controls at the screen's top. This should move into a [`Drawer`](https://ui.shadcn.com/docs/components/drawer) component for better touch screen UX [#13](https://github.com/openstatusHQ/data-table-filters/issues/13). 145 + 146 + To make the data table more accessible for React users, we need to create a simple vitejs example [#14](https://github.com/openstatusHQ/data-table-filters/issues/14) that doesn't rely on Nextjs (except maybe for the `/api` endpoint to fetch data from any server)! 147 + 148 + A fun feature is support for natural language filters [#15](https://github.com/openstatusHQ/data-table-filters/issues/15), allowing to write the filter and let AI translate into the correct `filter:query` using AI and the config models! 149 + 150 + Feel free to open a GitHub issue with feature requests or encountered bugs, or contribute directly by opening a PR. 151 + 152 + ### What's next? 153 + 154 + The `<Component />` API designs including the different `config` objects and `/api` endpoint standardization will drive our next bigger improvements. While I'm unsure when I'll have more time to focus on this, the current state makes for a perfect break point. 155 + 156 + Thanks for the read and see you in a while! And don’t forget to [leave a star](https://github.com/openstatusHQ/openstatus) if you enjoyed it! 157 + 158 + Try the demo: [logs.run/i](http://logs.run/i)