Modern React

Remix is a terrific framework, but it has one big downside. It's mixing compositional paradigms. You have nested routes and nested components.
In Remix, we get a lot of benefit out of splitting data management from the UI. However, we are stuck with the nested route boundaries as the place where this happens.
To work around this, we have the Full Stack Components pattern. This pattern takes advantage of Remix's resource route feature which simply allows us to get an API endpoint at a route provided it does not have a default export. So instead we export a component with a name and then that component is wired up to the route where it appears:
For a full example, check the blog post linked above, but here are the relevant bits from it:
import { json, type LoaderFunctionArgs } from '@remix-run/node'
import { useFetcher } from '@remix-run/react'
import { searchCustomers } from '#app/models/customer.server.ts'
import { requireUser } from '#app/session.server.ts'

export async function loader({ request }: LoaderFunctionArgs) {
	await requireUser(request)
	const url = new URL(request.url)
	const query = url.searchParams.get('query')
	invariant(typeof query === 'string', 'query is required')
	return json({
		customers: await searchCustomers(query),
	})
}

export function CustomerCombobox({ error }: { error?: string | null }) {
	const customerFetcher = useFetcher<typeof loader>()
	// ... other state and setup ...

	// Example of how the loader is called in the UI
	const handleInputChange = (changes) => {
		customerFetcher.submit(
			{ query: changes.inputValue ?? '' },
			{ method: 'get', action: '/resources/customers' },
		)
	}

	// ... rest of the component implementation ...
}
One problem here is we have to manually wire up the fetcher to the loader. Another issue with this pattern which is not depicted here is if we wanted to preload any of this data, we would have to add it as a prop to the component and then anywhere it's rendered, find the nearest parent route and add the data to that route's loader.
We can do better. With the right framework primitives, we can completely eliminate the network from our code.

React Server Components and Actions

React has always been the best composition paradigm. Rather than routes being the composition boundary, we can use React Server Components to compose our application.
Server components can manage loading our data and server actions can manage mutating it.
Using RSCs and server actions, we can compose these different pieces together at any level and everything simply becomes a React component.