Tips from 8 months of TanStack/Router in production
On my last day at Tia I wrote a master vision doc for our TanStack Router app. Here are the parts I can share.
These are battle-tested patterns and lessons learned from using TanStack Router in production since before v1. They worked well for us to iterate quickly, work as a team, and have shockingly few bugs. I can't know how they'll evolve in the future.
TanStack Router 101
If you need a refresher, TanStack Router is the modern React for the rest of us. It combines the best parts of Remix and NextJS for a wonderful type-safe routing experience with all the features you expect:
- type safe routing, you'll get squiggly lines when linking to a path that doesn't exist
- router coordinated data loading, you show 1 loading spinner for everything
- suspense-first design, no more
{isLoading ? ...}in your UI components - optional file-based routing, your code structure can follow your app structure
- nested routes, your URLs can control specific portions of the UI
UX vision for the app
Our app is based on the principles of multi-page apps. An important nuance makes it different to the multi page apps of yore – we aim to use a unique URL for every significant UI state, but we avoid full-page reloads and round-trips to the server.
This is where TanStack Router shines.
Goals:
- enable multi-tasking
- support easy context sharing
- empower unguided navigation through the app
We're building for power users: they don't need us to hold their hands, they need us to get out of the way. This is the mental image I like to use for what we're building 👇
Guiding principles:
- URL for every significant UI state
- every URL is independent
- cross-page app state is for performance
- a page should always rebuild itself on Cmd+R
- if you share the link with someone, they should see the same thing you did
- reduce clicks
- autofocus first field of forms
- support keyboarding through common tasks
- auto-navigate to next logical place on form submit
- prefer information density
- maintain mental context across operations
Code organization
The app follows a file-based routing setup from TanStack Router.
That means the file structure closely follows the URL structure, which makes things easy to find and fairly easy to collocate.
We try to organize code into vertical modules or components – every page folder should contain everything it needs to work. And every component or function should live at the nearest shared space in the hierarchy.
../../../ imports are the main code smell to look out for.
Horizontal concerns
We've identified a few horizontal concerns like aspects of loading shared data and certain UI atoms that don't belong to any particular page.
We try to treat those like libraries.
There is a shared /data/ and /pages/-components directory for these which have a path alias for easier imports. This smells a lot like a custom SDK or ORM + component library and should be split off when it matures. Avoid leaking page-specific concerns into these.
Data patterns
TanStack Router supports data loaders at the router level and implements first-party Suspense support.
In practice that means your components don't need to deal with their own loading and error states. You can build your components to focus on the happy path, then let the router handle loading and error states.
A few rules of thumb we’ve developed over time:
- use loaders for data that's required by the whole page
- use suspense queries for data used by a single component
- this lets the router handle coordination but keeps data scoped to your component
- you don't need to handle loading/error states
- use queries for data that depends on interaction
- you have to implement loading/error states
- good for typeaheads and such
- use queries for deferable data used by a single component
- you have to implement loading/error states
- makes app feel faster
- good for supplemental data
- share code between loaders and queries
- it makes life easier when you have a query function that works for both useQuery and loader
- helps you ensure consistent cache keys
- helps ensure consistent data access patterns for trickier things
- yes this starts to look a lot like we're building an SDK or ORM
Go gardening
We like to take a sit-back-and-observe approach. Avoid generalizing code until you've been repeating the same thing and it def looks like a pattern. This makes it easier to keep code flexible and making the abstractions we do build feeling more natural.
Hope this helps. It's a pile of app design guidelines I plan to keep in the next thing I touch.
Cheers,
~Swizec
