ホームページ > 記事 > ウェブフロントエンド > ゼロから立ち上げまで: リミックスを活用した開発の取り組みから得た重要なポイント
約 6 か月前、私は会社の Web アプリケーションの基盤として Remix を選択するという、一部の人が大胆な決断だと言うかもしれませんが、その決断を下しました。今日は、一歩下がって私たちが行った選択を振り返る時期が来たと思います。インフラストラクチャに関する主な決定事項を確認し、途中で実用的な使用例を少し散りばめます。
それでは、早速、この旅のハイライトとハイライト、つまり満足感と学んだ教訓を組み合わせて説明しましょう。
Remix は NextJS ほど普及しておらず、私の知る限り大企業で Remix を使用している例はあまりなかったため、これはおそらく私が当時行ったインフラストラクチャの決定の中で「最もリスクが高い」ものでした。
今日に遡って - ChatGPT はつい数日前に Next から Remix に移行しました!
前の記事で詳しく説明したように、私が Remix を選択した理由は数多くありますが、その理由の 1 つは、そのシンプルさ、「フルスタック」の側面 (つまり、リミックス サーバーを「フロントエンドのバックエンド」として利用すること)、およびその優れた抽象化です。ルーティング、データフェッチ、ミューテーション。
幸いなことに、リミックスは配信されましたか?このフレームワークは直感的で、簡単に学習して他の人に教えることができ、ベスト プラクティスが確実に使用されるため、コードの作成とテストの両方が簡単になります。
Remix との連携を開始してから数か月後、彼らは React Router との正式な統合を発表しました。これにより、vite への移行と同様に、さらに多くの人が React Router を使用するように説得されることを期待しています。
私にとって、リミックスが正しい選択であることが多くの場面で明らかになりました。私が最近取り組んだ実際的な例を 1 つ挙げます。リミックス サーバーで単一のロガー インスタンスを使用して、アプリ全体のアクションとエラーをログに記録して追跡し、監視機能を強化します。実装は非常に簡単でした:
ステップ 1 - ロガーを作成します (私の場合は、監視に使用する Datadog とうまく連携する winston を使用しました)
ステップ 2 - ロガーをサーバーのロードコンテキストに追加します (私の場合は高速でした):
app.all( '*', createRequestHandler({ getLoadContext: () => ({ logger, // add any other context variables here }), mode: MODE, // ... }), );
ステップ 3 (typescript ユーザー向け) - Remix のデフォルトの型定義を更新して、アプリの読み込みコンテキストにロガーを含めます
import '@remix-run/node'; import { type Logger } from 'winston'; declare module '@remix-run/node' { interface AppLoadContext { logger: Logger; } }
ステップ 4 - ルートのローダーまたはアクションで必要に応じてロガーを使用します!
export async function action({ request, context }: ActionFunctionArgs) { try { await someAction(); } catch (e) { context.logger.error(e); } }
このセクションを終える前に、ストリーミング データ/コンポーネント用の RSC の実装や、認証に最適な Route ミドルウェアなど、Remix にあればよかったのにまだ実現していない機能があることをお伝えしたいと思います。 /認可。幸いなことに、これらの機能 (およびその他の優れた機能) はロードマップで優先されているようですので、すぐに入手できることを願っています!
過去の前向きな経験に基づいて、@tanstack/react-query を選択するのは私にとって簡単な決断でした。今回も期待を裏切りませんでした。この API は多用途で拡張可能で、最良の点で偏見にとらわれないため、他のツールとの統合が容易になります。
私はこれがとても気に入ったので、Apollo クライアントというより明白な選択肢ではなく、内部 API が GraphQL ベースであることを知ってこれを選択しました。その理由については多くの理由があります。 Tanstack Query には優れた API があり、Apollo よりも大幅に軽量です。また、必要になった場合に備えて、GraphQL のような特定のテクノロジーに合わせて大幅に調整されたツールに依存したくなかったからです。他のテクノロジーを切り替えるか、組み込んでください。
さらに、Remix を使用しているため、Tanstack Query の SSR 機能を最大限に活用できます。つまり、クライアント側でこれらのクエリを変更、無効化、または再フェッチする機能を維持しながら、サーバー側でクエリをプリフェッチできます。簡略化した例を次に示します:
import { dehydrate, QueryClient, HydrationBoundary, useQuery } from '@tanstack/react-query'; import { json, useLoaderData } from '@remix-run/react'; const someDataQuery = { queryKey: ['some-data'], queryFn: () => fetchSomeData() } export async function loader() { const queryClient = new QueryClient(); try { await queryClient.fetchQuery(someDataQuery); return json({ dehydrate: dehydrate(queryClient) }); } catch (e) { // decide whether to handle the error or continue to // render the page and retry the query in the client } } export default function MyRouteComponent() { const { dehydratedState } = useLoaderData<typeof loader>(); const { data } = useQuery(someDataQuery); return ( <HydrationBoundary state={dehydratedState}> <SomeComponent data={data} /> </HydrationBoundary /> ); }
I was initially skeptical about Tailwind, having never used it before, and because I didn’t quite understand the hype (it seemed to me at first just like syntactic sugar over CSS). However, I decided to give it a try because of its strong recommendations and popularity within the community, and I’m really glad I did. Tailwind’s utility-first approach made it incredibly easy to build a consistent and robust design system right from the start, which, looking back, was a total game changer.
It also pairs perfectly with shadcn, which we used, and together they allowed me to deliver quickly while keeping everything modular and easy to modify later on - a crucial advantage in a startup environment.
I also really like how easy it is to customize tailwind's theme to your needs - for example, overriding tailwind's default scheme:
First, define your colors as variable's under tailwind's main .css file:
@tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { /* define the primitive design system tokens */ --colors-blue-100: hsl(188 76% 90%); --colors-blue-200: hsl(187 63% 82%); --colors-blue-25: hsl(185 100% 98%); --colors-blue-300: hsl(190 52% 74%); --colors-blue-400: hsl(190 52% 61%); --colors-blue-50: hsl(188 92% 95%); --colors-blue-500: hsl(190 74% 39%); --colors-blue-600: hsl(191 77% 34%); --colors-blue-700: hsl(190 51% 35%); --colors-blue-800: hsl(191 52% 29%); --colors-blue-900: hsl(190 51% 23%); --colors-blue-950: hsl(190 52% 17%); --colors-gray-100: hsl(0 0 90%); --colors-gray-200: hsl(0 0 85%); --colors-gray-25: hsl(0 0 98%); --colors-gray-300: hsl(0 0 73%); --colors-gray-400: hsl(0 1% 62%); --colors-gray-50: hsl(0 0 94%); --colors-gray-500: hsl(0 0% 53%); --colors-gray-600: hsl(0 0 44%); --colors-gray-700: hsl(0 0 36%); --colors-gray-800: hsl(0 2% 28%); --colors-gray-900: hsl(0 0 20%); --colors-gray-950: hsl(0 0 5%); --colors-red-100: hsl(4 93% 94%); --colors-red-200: hsl(3 96% 89%); --colors-red-25: hsl(12 100% 99%); --colors-red-300: hsl(4 96% 80%); --colors-red-400: hsl(4 92% 69%); --colors-red-50: hsl(5 86% 97%); --colors-red-500: hsl(4 88% 61%); --colors-red-600: hsl(4 74% 49%); --colors-red-700: hsl(4 76% 40%); --colors-red-800: hsl(4 72% 33%); --colors-red-900: hsl(8 65% 29%); --colors-red-950: hsl(8 75% 19%); /* ... */ /* define the semantic design system tokens */ --primary-light: var(--colors-blue-200); --primary: var(--colors-blue-600); --primary-dark: var(--colors-blue-800); --primary-hover: var(--colors-blue-50); --text-default-primary: var(--colors-gray-700); --text-default-secondary: var(--colors-gray-800); --text-default-tertiary: var(--colors-gray-900); --text-default-disabled: var(--colors-gray-300); --text-default-read-only: var(--colors-gray-400); --disabled: var(--colors-gray-300); --tertiary: var(--colors-gray-50); /* ... */ } }
Then, extend Tailwind's default theme via the tailwind config file:
import { type Config } from 'tailwindcss'; const ColorTokens = { BLUE: 'blue', GRAY: 'gray', RED: 'red', } as const; const generateColorScale = (colorName: string) => { const scales = [25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]; return scales.reduce( (acc, scale) => { acc[scale] = `var(--colors-${colorName}-${scale})`; return acc; }, {} as Record<string, string>, ); }; export const customColors = Object.values(ColorTokens).reduce((acc, color) => { return { ...acc, [color]: generateColorScale(color), }; }, {}); const config = { // ... additional config theme: { extend: { colors: customColors }, }, } satisfies Config; export default config;
This is just the tip of the iceberg - you can go on to define custom spacing, text sizing and much more!
Previously using Cypress, I was inclined to choose it, but I kept hearing hype around Playwright and figured I'll research it extensively before making a decision. After comparing Playwright with Cypress, it was clear Playwright is the right choice to make - the fact it comes with parallel execution out of the box, the broader browser support, running times and debugging capabilities - all made Playwright the obvious choice.
And, while this is very subjective, I like Playwright's syntax much better. I find it similar to React Testing Library's syntax, which I like, and I tend to think the tests are a lot more readable, with the asynchronous aspect of the tests being very straight forward, unlike the syntax of Cypress that can cause tests to feel bloated by .then() statements and subsequent indentations.
I think my favorite feature of Playwright is their implementation of Test Fixtures. They provide a clean way to initialize and reuse resources like page objects, making tests more modular and maintainable. Make sure to check out the above link to learn more about it!
First off, let me clarify — @tanstack/react-table is a fantastic tool, which is why I was inclined to choose it in the first place, but it wasn’t the best fit for my particular use case. The very features that make it great, like its small bundle size and customizable API, ended up being less relevant to our needs than I originally thought. Despite having full control of the rendering of the Table, I was having some issues aligning its scrolling behavior to our desired outcome (why is it still not possible in 2024 to have a