b4s36t4
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # How to build React SSR Application with Vite? - **meta-title**: *React SSR Application with vite* - **meta-description**: *Learning how to build a React Server side rendering app using vite, routing powered by React Router and APIs powered by Strapi.* - **Figma images**: ![How to build React SSR Application with Vite_-1](https://hackmd.io/_uploads/HyuXfomMC.png) ![How to build React SSR Application with Vite_](https://hackmd.io/_uploads/BkuQGi7MA.png) - **Publish date**: - **Reviewers**: ## Introduction While **single-page applications (SPA)** are great, they lack support for good page speeds and SEO, which are critical to businesses and bring more revenue. With **server-side rendering (SSR)**, we can build applications with good SEO and speed. In this tutorial, we'll learn to build a meta-framework, like Next.js, with the following Features: A meta-framework is a development tool or framework built out of another framework. Examples include Next.js from React.js, Nuxt.js from Vue.js, and so on. * Setting up a server to render the HTML from React. * Support meta tags. * Routing Logic. * Render dynamic content on the server side with Strapi. ## Prerequisites Understanding this blog requires some basic knowledge of the following topics: * [NodeJS](https://nodejs.org) installed on system. * Good Understanding of [ReactJS](https://react.dev), **Javascript** and [Express.js](https://expressjs.com). * Knowledge about Bundlers, specifically about [Vite](https://vitejs.dev). * Basic understanding of [Strapi](https://strapi.io). * Server-side rendering and Client-s rendering. * TypeScript: This blog is entirely based on Typescript-supported React, so please make sure you get an introduction to Typescript [here](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html). It's not necessary, but it's good to have. ## Why Vite? [Vite](https://vitejs.dev/) is a popular build tool to convert applications like React to browser understandable code. Vite comes with great plugin system and great community to add more to it many popular frameworks. Qwik and Remix for example, are using Vite as their build tool to create production code. ## React SSR When we create a React app either from biolerplates or from scratch we should be installing two dependencies i.e [`react`](https://www.npmjs.com/package/react) and [`react-dom`](https://www.npmjs.com/package/react-dom). `react-dom` is the responsible one for converting React code into HTML code either on the server or client. `react-dom` prodivdes a function `renderToString` from `react-dom/server` which we'll be using to convert the React to HTML. ## Setup If you alreay have a Vite powered React app, you don't need to read this section, please forward to Next Section. To create a React App powered by Vite, run the following command in your terminal or CMD and follow the questions promptly to set up the Vite-powered App. Please make sure to choose the `React` when asked for `framework` and choose `Typescript`. ```bash npm create vite@latest ``` We're using `react-router` to implement routing, make sure you install `react-router-dom` into dependencies to run the application. ```shell! npm i react-router-dom ``` ### Creating Routes As for the demo purpose, we're going to have a simple routing logic which is going to have 2 main routes & 1 one nested route. We will be going to use [Data API Supported Routers](https://reactrouter.com/en/main/routers/picking-a-router#using-v64-data-apis) > Data API routers useful for which the pages have heavy use of data, either loading the data or data mutations. These routers support loading data into routes or handle the mutations within component itself. To learn more click [here](https://github.com/remix-run/react-router/discussions/10204) Here I'm picking `createBrowserRouter` and will be creating all my routes in a different file i.e I'm calling is `routes.jsx`. > Defining Routes in a different place is useful because we have to re-use the same routes in multiple places i.e we have to create two routers one for server and one for client, defining routes in different place allows us to share the routes while using the different routers. ```javascript! // location: src/routes.jsx import axios from "axios"; import { IndexRouteObject, NonIndexRouteObject, json } from "react-router-dom"; import { Home } from "./Home"; type MetaAttributes = "name" | "http-equiv" | "charset" | "itemprop"; type Meta = { title: string } | Partial<Record<MetaAttributes, string>>; export type MetaRouteObject = | (IndexRouteObject & { meta?: React.ReactNode | Meta[] }) | (NonIndexRouteObject & { meta?: React.ReactNode | Meta[]; children?: MetaRouteObject[]; }); const strapiService = axios.create({ baseURL: "http://127.0.0.1:1337/api" }); export const routes: MetaRouteObject[] = [ { path: "/", element: <Home />, loader: async () => { const foodList = ( await strapiService.get<StrapiResponse<Food[]>>("/foods") ).data; return json(foodList.data ?? []); }, meta: ( <> <title>Home</title> <meta name="title" content="Vite Server | Home" /> </> ), children: [ { path: "home", element: ( <p> Nested Screen, rendered on <kbd>/home</kbd> </p> ), meta: ( <> <title>Nested Home</title> <meta name="title" content="Vite Server" /> </> ), }, ], }, { path: "*", element: <p>404, wrong Landing</p>, meta: [{ title: "Hello" }, { name: "Hello" }], }, ]; ``` The above code let's us create routes for the `react-router` while extending support for the use of `meta` tag supporting individual routes dynamically. `meta` property can be any a React component i.e JSX or attributes supported by `meta` tag. Providing support of `meta` tags let's us improve the SEO of our webpage giving dynamic preview in social media platforms like `X` or `WhatsApp` etc. `MetaRouteObject` is an extension of Type Provided by `react-router`'s `RouteObject` to support typings for the new `meta` property we've added. As we're using the `Data APIs supported routes`, we should be executing the loaders at server-side, for that we need to write a function which will execute the `loader` data and push it into the `Router`'s context To keep things simple, we're going to create the function in a different file i.e `fetch.ts` ```javascript! // location: src/fetch.ts import type * as express from "express"; export function createFetchRequest( req: express.Request, res: express.Response ): Request { const origin = `${req.protocol}://${req.get("host")}`; // Note: This had to take originalUrl into account for presumably vite's proxying const url = new URL(req.originalUrl || req.url, origin); const controller = new AbortController(); res.on("close", () => controller.abort()); const headers = new Headers(); for (const [key, values] of Object.entries(req.headers)) { if (values) { if (Array.isArray(values)) { for (const value of values) { headers.append(key, value); } } else { headers.set(key, values); } } } const init: RequestInit = { method: req.method, headers, signal: controller.signal, }; if (req.method !== "GET" && req.method !== "HEAD") { init.body = req.body; } return new Request(url.href, init); } ``` The above function is used to transform `express` (we're using to convert react to html and serve html) and transform it to `Request` object which we can use This `fetch` function will be used by `React Router` to execute the `loader` data and push into the Router's Context. ### Decoupling Server and Client. What we have created using the above command is a Client Side app which now we need to decouple it to server & client. `Vite` uses `main.tsx` or `main.jsx` as entry file, which we need to replace with two files `main-client.tsx` and `main-server.tsx`. > File names can be anything, but just ensure we can simply differentiate between client & server easily. ### Client Code ```javascript // name: main-client.tsx import React from "react"; import { hydrateRoot } from "react-dom/client"; import { RouterProvider, createBrowserRouter } from "react-router-dom"; import "./index.css"; import { routes } from "./routes.tsx"; // creating Client-side Router const router = createBrowserRouter(routes); // Hydrating the component at client-side hydrateRoot( document.getElementById("root")!, <React.StrictMode> // Client-side Routers <RouterProvider router={router} fallbackElement={null} /> </React.StrictMode> ); ``` ### Server Code ```javascript import React from "react"; import { type StaticHandlerContext, StaticRouterProvider, createStaticRouter, createStaticHandler, } from "react-router-dom/server"; import { MetaRouteObject, routes } from "./routes.tsx"; import { createFetchRequest } from "./fetch.ts"; import type * as express from "express"; import { renderToString } from "react-dom/server"; import "./index.css"; interface RenderProps { request: express.Request; response: express.Response; } const handler = createStaticHandler(routes); // Generate React-Component for the `meta` tags from `routes.jsx` file. const getMeta = (matches: StaticHandlerContext["matches"]) => { let matchedRoute = matches[matches.length - 1].route as MetaRouteObject; // If there's any index route, we try to read the meta from it's parent route. // If an index route is inside matched routes, just if (matchedRoute.index) { matchedRoute = matches[matches.length - 2].route as MetaRouteObject; } if (Array.isArray(matchedRoute.meta)) { return ( <> {matchedRoute.meta.map((meta, index) => { return <meta key={index} {...meta} />; })} </> ); } return matchedRoute.meta; }; // Function responsible to render server-side. export async function render({ request, response }: RenderProps) { // execute `loader` data for the matched route. const remixRequest = createFetchRequest(request, response); const context = await handler.query(remixRequest); if (context instanceof Response) { throw context; } // Server-Side Router const router = createStaticRouter( handler.dataRoutes, context as StaticHandlerContext ); const metaComponent = getMeta(context.matches); // Render the meta component into server-side const headContent = renderToString(metaComponent); // Generate Main HTML const html = renderToString( <React.StrictMode> <StaticRouterProvider router={router} context={context} /> </React.StrictMode> ); return { html, head: headContent }; } ``` ### Creating a Server Now that we have all things required form the react side to generate the SSR app, we're missing the server which is responsible to serve the generated HTML. We'll be using the `express` we've installed before to serve the app using `vite`. Create a file called `server.js` in the root folder of your app and copy the below contents to into the file. ```javascript! // name: server.js import express from "express"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "node:url"; import { createServer as createViteServer } from "vite"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const port = process.env.PORT || 5173; const base = process.env.BASE || "/"; /** * @type {string} */ let template; /** * @type {import("vite").ViteDevServer | null} */ let vite; const isProduction = process.env.NODE_ENV === "production"; const app = express(); async function loadTemplate() { if (isProduction) { template = fs.readFileSync( path.resolve(__dirname, "client/index.html"), "utf-8" ); const compression = (await import("compression")).default; const sirv = (await import("sirv")).default; app.use(compression()); app.use(base, sirv("./dist/client", { extensions: [] })); } else { vite = await createViteServer({ appType: "custom", server: { middlewareMode: true }, }); app.use(vite.middlewares); template = fs.readFileSync(path.resolve(__dirname, "index.html"), "utf-8"); } return vite; } const startServer = async () => { await loadTemplate(); app.use("*", async (req, res) => { let render, templateHTML; if (isProduction) { render = (await import("./dist/server/entry-server.js")).render; templateHTML = template; } else { templateHTML = await vite.transformIndexHtml(req.originalUrl, template); render = (await vite.ssrLoadModule("./src/entry-server.tsx")).render; } const { html: appHtml, head } = await render({ request: req, response: res, }); // 5. Inject the app-rendered HTML into the template. let html = templateHTML.replace(`<!--ssr-outlet-->`, appHtml); html = html.replace( "<!--meta-outlet-->", head ?? "<title>React SSR App </title>" ); // 6. Send the rendered HTML back. res.status(200).set({ "Content-Type": "text/html" }).end(html); }); app.listen(port); console.log(`Checkout: http://localhost:${port}`); }; startServer(); ``` The above file contains the code related to serving the App in the both `development` and `production` mode. The main difference in between the both modes are just that how we load the files. In `development` mode we load the react code and transpile it on the go whereas the in `production` mode the code is already compiled to `javascript` and can be easily convereted into HTML without any step required from vite to `transpile` the code. **Update `Package.json`** We should be updating our package.json file to support new npm commands to be able to spin up a express server to build and server the html ```json // Package.json content "scripts":{ //"...existing scripts" "dev": "node server", "build:client": "vite build --outDir dist/client", "build:server": "vite build --outDir dist/server --ssr src/entry-server.tsx", "build": "npm run build:client && npm run build:server", "serve":"NODE_ENV=production node server" } ``` Now that we have all code setup for serving the SSR application, let's load the application with some API data using [Strapi](https://strapi.io/) Please follow this [guide](https://docs.strapi.io/dev-docs/quick-start) to install strapi and run things quicker. In our example, we're going to create a new Content-Type called `Food` which will going to have following properties. * name * category * recipe The aim is to load the food items from `strapi` on Server Side. ### Creating Food Content-type ![Screenshot 2024-05-07 at 6.20.23 PM](https://hackmd.io/_uploads/rJJj7jvfC.png) As I have stated above, we should be creating a content-type with the properties `name`, `category` and `recipe` which we use to serve the API. > Please read this [guide](https://docs.strapi.io/dev-docs/api/rest) to understand how to access the API's for the newly created content. **Please make sure you create some sample data before hitting the API.** ![Screenshot 2024-05-07 at 6.22.18 PM](https://hackmd.io/_uploads/ry1GEoPMA.png) ### Consuming the API To load the content on server-side we should be using `loader` property in the `routes.jsx`. I'm going to consume the API for home route i.e `/` path. > http://localhost:1337/api/foods is the API we should be hitting. After doing the changes `routes.jsx` should look like below ```javascript! // name: routes.jsx // // Make sure to install `axios`. import axios from 'axios' const strapiService = axios.create({ baseURL: "http://127.0.0.1:1337/api" }); const routes = [ { path: "/", element: <Home />, loader: async () => { const foodList = ( await strapiService.get<StrapiResponse<Food[]>>("/foods") ).data; return json(foodList.data ?? []); }, meta: ( <> <title>Home</title> <meta name="title" content="Vite Server | Home" /> </> ), }, ...children // ....existing routes ] ``` We're using `<Home />` as `element` for path `/`, but we haven't created it anywhere, let's do that now. #### `<Home />` component ```javascript! import { Outlet, useLoaderData } from "react-router-dom"; export const Home = () => { const foodList = useLoaderData(); if (!foodList) { return <p>No Food items found</p>; } return ( <> {(foodList as Food[]).map((food: Food) => { return ( <div key={food.id} style={{ border: "1px solid red", padding: "4px", marginBottom: 5, borderRadius: 5, }} > <p>{food.attributes.name}</p> <p>{food.attributes.category}</p> <p>{food.attributes.recipe}</p> </div> ); })} <Outlet /> </> ); }; ``` Here we're using `useLoaderData` function to read the data which was returned from `loader` function and then rendering based on the data. This all while no API call happening at browser, all from server 🚀. ### Building & Serving To build the application we need to build for `Server` and `Client` both of them. The commands to build for production is available as a npm script which we can use as follows. ```shell! npm run build ``` To serve the application we need a server runtime i.e A system where we can run `Node.js` Server to run the production build of Application we should be having `NODE_ENV` variable value to `PRODUCTION`. To keep things simple we can simple run the npm script `preview` command i.e ```shell! npm run serve ``` > For production we're using `compression` package which gives the support for `gzip` compression to reduce the data transfer and compression. ## Demo. ![ezgif-3-53b4956760 (1)](https://hackmd.io/_uploads/rkrvxluGC.gif) **You can access code for this blog [here](https://github.com/b4s36t4/Strapi-SSR-Demo)** ### References [ViteJS](https://vitejs.dev) [React Router](https://reactrouter.com) [SSR Examples](https://github.com/bluwy/create-vite-extra/tree/master/template-deno-react-ts) ## Conclusion While Meta frameworks like `Next.js` and `Remix` are very good, we should know how they're working under the hood and how we can implement them with whatever tools we have. Implementations may vary but the way we do with what tools we implement is how things work. Here we've used `Vite` and `Router Dom` which are very great tools what they're built for along with the things what advanced. # Review * Please give a detailed installation process of the project. * The GIF you provided is not clear enough. Could you please share with me on Github since Hackmd supports an upload of 1MB.

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully