|
| 1 | +# 04 TanStack Start migration |
| 2 | + |
| 3 | +In this example we are going to migrate our previous TanStack Router example to TanStack Start, which is a framework that combines Vite, TanStack Router, and other TanStack libraries for building SSR applications. |
| 4 | + |
| 5 | +We will start from `03-boilerplate`. |
| 6 | + |
| 7 | +# Steps to build it |
| 8 | + |
| 9 | +`npm install` to install previous sample packages: |
| 10 | + |
| 11 | +```bash |
| 12 | +npm install |
| 13 | +``` |
| 14 | + |
| 15 | +Install TanStack Start: |
| 16 | + |
| 17 | +```bash |
| 18 | +npm uninstall @tanstack/router-plugin |
| 19 | + |
| 20 | +npm install @tanstack/react-start --save |
| 21 | +``` |
| 22 | + |
| 23 | +> The `@tanstack/router-plugin` is not needed anymore as TanStack Start has built-in support for TanStack Router. |
| 24 | +> |
| 25 | +> [Quick Start Guide](https://tanstack.com/start/latest/docs/framework/react/quick-start) |
| 26 | +> |
| 27 | +> [Build from Scratch](https://tanstack.com/start/latest/docs/framework/react/build-from-scratch) |
| 28 | +
|
| 29 | +Update `vite.config.ts` to use TanStack Start plugin: |
| 30 | + |
| 31 | +_./vite.config.ts_ |
| 32 | + |
| 33 | +```diff |
| 34 | +- import { tanstackRouter } from '@tanstack/router-plugin/vite'; |
| 35 | ++ import { tanstackStart } from '@tanstack/react-start/plugin/vite'; |
| 36 | +import react from '@vitejs/plugin-react'; |
| 37 | +import { defineConfig } from 'vite'; |
| 38 | + |
| 39 | +export default defineConfig({ |
| 40 | + plugins: [ |
| 41 | +- tanstackRouter({ |
| 42 | +- target: 'react', |
| 43 | +- autoCodeSplitting: true, |
| 44 | +- }), |
| 45 | ++ tanstackStart(), |
| 46 | + react(), |
| 47 | + ], |
| 48 | + css: { |
| 49 | + modules: { |
| 50 | + localsConvention: 'camelCase', |
| 51 | + }, |
| 52 | + }, |
| 53 | +- server: { |
| 54 | +- proxy: { |
| 55 | +- '/api': 'http://localhost:3001', |
| 56 | +- }, |
| 57 | +- }, |
| 58 | +}); |
| 59 | +``` |
| 60 | + |
| 61 | +Since TanStack Start handles routing for server-side and client-side, we need to move the router configuration to a dedicated file: |
| 62 | + |
| 63 | +_./src/router.ts_ |
| 64 | + |
| 65 | +```tsx |
| 66 | +import { createRouter } from '@tanstack/react-router'; |
| 67 | +import { routeTree } from './routeTree.gen'; |
| 68 | + |
| 69 | +export function getRouter() { |
| 70 | + return createRouter({ |
| 71 | + routeTree, |
| 72 | + scrollRestoration: true, |
| 73 | + }); |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +> `scrollRestoration` is optional, it enables automatic scroll position restoration when navigating simulating browser behavior. |
| 78 | +
|
| 79 | +If we run the app now, we should see it partially working: |
| 80 | + |
| 81 | +```bash |
| 82 | +npm start |
| 83 | +``` |
| 84 | + |
| 85 | +However, we need to remove the SPA entrypoints (index.html and index.tsx) and move this configuration to the **\_\_root.tsx** file: |
| 86 | + |
| 87 | +- Remove `./src/index.tsx` |
| 88 | +- Remove `./index.html` |
| 89 | + |
| 90 | +And move the HTML configuration to the root route: |
| 91 | + |
| 92 | +_./src/routes/\_\_root.tsx_ |
| 93 | + |
| 94 | +```diff |
| 95 | +import { |
| 96 | +- Outlet, |
| 97 | + createRootRoute, |
| 98 | ++ HeadContent, |
| 99 | ++ Scripts, |
| 100 | +} from '@tanstack/react-router'; |
| 101 | +import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'; |
| 102 | +import * as React from 'react'; |
| 103 | ++ import normalizeCss from 'normalize.css?url'; |
| 104 | ++ import materialIcons from './material-icons.css?url'; |
| 105 | + |
| 106 | +export const Route = createRootRoute({ |
| 107 | ++ head: () => ({ |
| 108 | ++ meta: [ |
| 109 | ++ { charSet: 'utf-8' }, |
| 110 | ++ { name: 'viewport', content: 'width=device-width, initial-scale=1' }, |
| 111 | ++ { title: 'Rent a car' }, |
| 112 | ++ ], |
| 113 | ++ links: [ |
| 114 | ++ { rel: 'icon', type: 'image/png', href: '/home-logo.png' }, |
| 115 | ++ { rel: 'stylesheet', href: normalizeCss }, |
| 116 | ++ { rel: 'stylesheet', href: materialIcons }, |
| 117 | ++ ], |
| 118 | ++ }), |
| 119 | +- component: RootComponent, |
| 120 | ++ shellComponent: RootComponent, |
| 121 | +}); |
| 122 | + |
| 123 | +- function RootComponent() { |
| 124 | ++ function RootComponent({ children }: { children: React.ReactNode }) { |
| 125 | + return ( |
| 126 | +- <React.Fragment> |
| 127 | +- <div>Hello "__root"!</div> |
| 128 | +- <Outlet /> |
| 129 | +- <TanStackRouterDevtools /> |
| 130 | +- </React.Fragment> |
| 131 | ++ <html lang="en"> |
| 132 | ++ <head> |
| 133 | ++ <HeadContent /> |
| 134 | ++ </head> |
| 135 | ++ <body> |
| 136 | ++ <main>{children}</main> |
| 137 | ++ <TanStackRouterDevtools /> |
| 138 | ++ <Scripts /> |
| 139 | ++ </body> |
| 140 | ++ </html> |
| 141 | + ); |
| 142 | +} |
| 143 | + |
| 144 | +``` |
| 145 | + |
| 146 | +> [Application Root](https://tanstack.com/start/latest/docs/framework/react/build-from-scratch#the-root-of-your-application) |
| 147 | +> |
| 148 | +> [Import global CSS styles](https://tanstack.com/start/latest/docs/framework/react/guide/tailwind-integration#import-the-css-file-in-your-__roottsx-file) |
| 149 | +> |
| 150 | +> [Vite URL importing](https://vite.dev/guide/assets#explicit-url-imports) |
| 151 | +
|
| 152 | +As we saw we cannot resolve the car list fetching because we removed the proxy configuration from `vite.config.ts`. Let's add an environment variable to define the API URL: |
| 153 | + |
| 154 | +_./.env.local_ |
| 155 | + |
| 156 | +```env |
| 157 | +BASE_API_URL=http://localhost:3001/api |
| 158 | +BASE_PICTURES_URL=http://localhost:3001 |
| 159 | +
|
| 160 | +``` |
| 161 | + |
| 162 | +And update the route loader to use this environment variable: |
| 163 | + |
| 164 | +_./src/routes/cars/index.tsx_ |
| 165 | + |
| 166 | +```diff |
| 167 | +import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'; |
| 168 | ++ import { api } from '#pods/car-list'; |
| 169 | + |
| 170 | +- const getCarList = async () => |
| 171 | +- await fetch('/api/cars').then((res) => res.json()); |
| 172 | + |
| 173 | +export const Route = createFileRoute('/cars/')({ |
| 174 | +- loader: () => getCarList(), |
| 175 | ++ loader: () => api.getCarList(), |
| 176 | + component: RouteComponent, |
| 177 | +}); |
| 178 | +... |
| 179 | + |
| 180 | +``` |
| 181 | + |
| 182 | +It looks similar to the SPA version, but now we have SSR capabilities, check it by opening the browser dev tools and see the Network tab, the initial HTML response should contain the car list data. |
| 183 | + |
| 184 | +We can navigate to car details and see that working in SPA mode after the initial load. So, if we come back to the car list, it fails to fetch the data again because the env variables are not available in the browser by default. We need to expose them by prefixing them with `VITE_` or with a custom prefix configured in `vite.config.ts`: |
| 185 | + |
| 186 | +> It means that loaders functions run on the server and the client |
| 187 | +> |
| 188 | +> [Isomorphic by default](https://tanstack.com/start/latest/docs/framework/react/guide/execution-model#core-principle-isomorphic-by-default) |
| 189 | +
|
| 190 | +_./.env.local_ |
| 191 | + |
| 192 | +```diff |
| 193 | +- BASE_API_URL=http://localhost:3001/api |
| 194 | ++ PUBLIC_BASE_API_URL=http://localhost:3001/api |
| 195 | +- BASE_PICTURES_URL=http://localhost:3001 |
| 196 | ++ PUBLIC_BASE_PICTURES_URL=http://localhost:3001 |
| 197 | + |
| 198 | +``` |
| 199 | + |
| 200 | +_./vite.config.ts_ |
| 201 | + |
| 202 | +```diff |
| 203 | +import { tanstackStart } from '@tanstack/react-start/plugin/vite'; |
| 204 | +import react from '@vitejs/plugin-react'; |
| 205 | +import { defineConfig } from 'vite'; |
| 206 | + |
| 207 | +export default defineConfig({ |
| 208 | + plugins: [tanstackStart(), react()], |
| 209 | + css: { |
| 210 | + modules: { |
| 211 | + localsConvention: 'camelCase', |
| 212 | + }, |
| 213 | + }, |
| 214 | ++ envPrefix: 'PUBLIC_', |
| 215 | +}); |
| 216 | + |
| 217 | +``` |
| 218 | + |
| 219 | +_./src/core/env.constants.ts_ |
| 220 | + |
| 221 | +```diff |
| 222 | +export const ENV = { |
| 223 | + BASE_API_URL: |
| 224 | +- process.env.BASE_API_URL || |
| 225 | ++ process.env.PUBLIC_BASE_API_URL || |
| 226 | ++ import.meta.env.PUBLIC_BASE_API_URL || |
| 227 | + '', |
| 228 | + BASE_PICTURES_URL: |
| 229 | +- process.env.BASE_PICTURES_URL || |
| 230 | ++ process.env.PUBLIC_BASE_PICTURES_URL || |
| 231 | ++ import.meta.env.PUBLIC_BASE_PICTURES_URL || |
| 232 | + '', |
| 233 | +}; |
| 234 | + |
| 235 | +``` |
| 236 | + |
| 237 | +> [Vite Env Variables](https://vite.dev/guide/env-and-mode) |
| 238 | +> |
| 239 | +> [Use server functions if we want execute code only on the server](https://tanstack.com/start/latest/docs/framework/react/guide/server-functions) |
| 240 | +
|
| 241 | +Let's change the routes to apply styles and components that we already have in pods: |
| 242 | + |
| 243 | +_./src/routes/cars/route.tsx_ |
| 244 | + |
| 245 | +```diff |
| 246 | +- import { createFileRoute, Outlet } from '@tanstack/react-router'; |
| 247 | ++ import { createFileRoute, Link, Outlet } from '@tanstack/react-router'; |
| 248 | ++ import classes from './route.module.css'; |
| 249 | + |
| 250 | +export const Route = createFileRoute('/cars')({ |
| 251 | + component: RouteComponent, |
| 252 | +}); |
| 253 | + |
| 254 | +function RouteComponent() { |
| 255 | + return ( |
| 256 | + <> |
| 257 | +- <div style={{ background: 'teal' }}>Common layout</div> |
| 258 | ++ <nav className={classes.nav}> |
| 259 | ++ <Link className={classes.link} to="/"> |
| 260 | ++ <img src="/home-logo.png" alt="logo" width={32} height={23} /> |
| 261 | ++ </Link> |
| 262 | ++ <h1 className={classes.title}>Rent a car</h1> |
| 263 | ++ </nav> |
| 264 | ++ <div className={classes.content}> |
| 265 | + <Outlet /> |
| 266 | ++ </div> |
| 267 | + </> |
| 268 | + ); |
| 269 | +} |
| 270 | + |
| 271 | +``` |
| 272 | + |
| 273 | +> [Migrating from Nextjs Guide](https://tanstack.com/start/latest/docs/framework/react/migrate-from-next-js) |
| 274 | +
|
| 275 | +Update the car list page: |
| 276 | + |
| 277 | +_./src/routes/cars/index.tsx_ |
| 278 | + |
| 279 | +```diff |
| 280 | +- import { api } from '#pods/car-list'; |
| 281 | ++ import { api, CarList, mapCarListFromApiToVm } from '#pods/car-list'; |
| 282 | +- import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'; |
| 283 | ++ import { createFileRoute } from '@tanstack/react-router'; |
| 284 | + |
| 285 | +export const Route = createFileRoute('/cars/')({ |
| 286 | ++ head: () => ({ |
| 287 | ++ meta: [{ title: 'Rent a car - Car list' }], |
| 288 | ++ }), |
| 289 | + loader: () => api.getCarList(), |
| 290 | + component: RouteComponent, |
| 291 | +}); |
| 292 | + |
| 293 | +function RouteComponent() { |
| 294 | +- const navigate = useNavigate(); |
| 295 | + const cars = Route.useLoaderData(); |
| 296 | + |
| 297 | +- return ( |
| 298 | +- <> |
| 299 | +- <ul> |
| 300 | +- {cars.map((car) => ( |
| 301 | +- <li key={car.id}> |
| 302 | +- <Link to="/cars/$id" params={{ id: car.id }}> |
| 303 | +- {car.name} |
| 304 | +- </Link> |
| 305 | +- </li> |
| 306 | +- ))} |
| 307 | +- </ul> |
| 308 | +- <button onClick={() => navigate({ to: '/' })}>Go back to home</button> |
| 309 | +- </> |
| 310 | +- ); |
| 311 | ++ return <CarList carList={mapCarListFromApiToVm(cars)} />; |
| 312 | +} |
| 313 | + |
| 314 | +``` |
| 315 | + |
| 316 | +> [Head Management](https://tanstack.com/router/latest/docs/framework/react/guide/path-params#seo-and-canonical-urls) |
| 317 | +
|
| 318 | +Update the car details page: |
| 319 | + |
| 320 | +_./src/routes/cars/$id.tsx_ |
| 321 | + |
| 322 | +```diff |
| 323 | ++ import { api, Car, mapCarFromApiToVm } from '#pods/car'; |
| 324 | +import { createFileRoute } from '@tanstack/react-router'; |
| 325 | + |
| 326 | +export const Route = createFileRoute('/cars/$id')({ |
| 327 | ++ loader: ({ params }) => api.getCar(params.id), |
| 328 | ++ head: ({ loaderData }) => ({ |
| 329 | ++ meta: [{ title: `Rent a car - Car ${loaderData?.name} details` }], |
| 330 | ++ }), |
| 331 | + component: RouteComponent, |
| 332 | +}); |
| 333 | + |
| 334 | +function RouteComponent() { |
| 335 | +- const { id } = Route.useParams(); |
| 336 | ++ const car = Route.useLoaderData(); |
| 337 | +- return <div>Car id={id}</div>; |
| 338 | ++ return <Car car={mapCarFromApiToVm(car)} />; |
| 339 | +} |
| 340 | + |
| 341 | +``` |
| 342 | + |
| 343 | +# About Basefactor + Lemoncode |
| 344 | + |
| 345 | +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. |
| 346 | + |
| 347 | +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. |
| 348 | + |
| 349 | +[Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. |
| 350 | + |
| 351 | +For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend |
0 commit comments