Tutorials

How to Save Column Order in a Retool Table

OTC Team··4 min read
How to Save Column Order in a Retool Table

If you've tried to save column order in a Retool table, you've hit one of the platform's most frustrating limitations. Users can drag and drop columns to reorder them in view mode, but the moment they refresh the page, the table snaps back to the original order defined in the editor. There's no built-in "save view" option. This post walks you through the best available workaround: decoupling column order using arbitrary keys and persisting the user's preference in localStorage.

Why Retool Doesn't Persist Column Order by Default

When a user reorders columns in view mode, Retool does update the table's columns property in memory — but it never writes that change back to the saved app. This is consistent with how Retool handles most view-mode property changes: they're temporary and session-scoped. So while you can read table1.columns to see the current order a user has set, that order is gone on the next page load. The legacy table component exposes the columns property, which makes a workaround possible. The new table component currently has no equivalent, which makes this even harder.

The Core Idea: Decouple Column Keys from Column Data

The workaround relies on one key insight: Retool locks column order to column keys. If your table data has keys like name, email, and status, Retool will always render them in the order those keys were first registered. You can't reorder them by reordering the object keys at runtime.

The fix is to give your table arbitrary, stable keys — like 0, 1, 2 — and then populate the data and headers for those columns dynamically based on an order array stored in localStorage. This way, Retool's internal column order never changes, but the data and headers displayed in each column do.

Step-by-Step: Saving Column Order with localStorage

  • Step 1 — Define a column name array. Create a columnNames state variable or transformer. On page load, check localStorage for a previously saved order. If one exists, use it. If not, fall back to the default key order from your query. For example: localStorage.getItem('columnOrder') ? JSON.parse(localStorage.getItem('columnOrder')) : Object.keys(query1.data[0])
  • Step 2 — Transform your query data. Write a transformer that remaps your query results so that each row uses the arbitrary numeric keys (0, 1, 2, etc.) instead of the real column names. The value at key 0 should be the value from whichever real column is first in columnNames.data. This transformed dataset is what you pass to the table's Data field.
  • Step 3 — Set column titles dynamically. In the table editor, for each column's Title field, reference columnNames.data[0], columnNames.data[1], etc. This ensures the header labels reflect the real column names even though the underlying keys are arbitrary numbers.
  • Step 4 — Save the new order on reorder. When a user drags a column, Retool updates table1.columns in memory. Add a button or use an event handler to capture this new order and write it back to localStorage. The logic looks roughly like this: read Object.values(table1.columns), filter for only your arbitrary keys using something like .filter(col => !isNaN(col.name)), map each key back to its real column name using columnNames.data, then call localStorage.setItem('columnOrder', JSON.stringify(newOrder)).
  • Step 5 — Reload the transformer on save. After writing to localStorage, trigger a re-evaluation of your columnNames transformer so the table immediately reflects the saved state without a full page refresh.

Filtering Stale Columns from table1.columns

One gotcha: Retool stores an entry in table1.columns for every key that has ever been passed to that table since it was created — even keys from old queries or previous data shapes. When you read table1.columns to capture a new order, make sure you filter it down to only the keys you're actively using. If your arbitrary keys are numeric indices, .filter(col => !isNaN(col.name)) works well. If you use a custom naming scheme (like col_0, col_1), write a filter that matches your prefix.

Limitations to Know Before You Build

This workaround works with the legacy Retool table component only. The new table component does not expose a columns property in the same way, and as of the time of writing, there is no equivalent workaround available. If your app is already using the new table, you're waiting on Retool to ship a native persistent column order feature — which has been requested but not yet prioritized by their team.

Also note that localStorage is browser- and device-scoped. If a user switches browsers or devices, their saved column order won't follow them. For true per-user persistence, you'd need to save the order to a database table keyed by user ID and load it on app initialization via a query.

Is It Worth the Complexity?

That depends on your users. If your internal tool is used daily by people who have strong preferences about column order — think ops teams, support agents, or finance analysts — the UX payoff is real. The setup takes an hour or two to wire up correctly, and the result is a table that remembers exactly how each user left it. If your use case is simpler, you might be better off waiting for Retool to build this natively. Either way, understanding why the default behavior works the way it does will save you a lot of debugging time.

Ready to build?

We scope, design, and ship your Retool app — fast.

Ready to ship your first tool?