ReactJS Server Side Rendering

SPA uygulamalarımızda, uygulama publish edildikten sonra tüm proje bir bundle dosyasına sıkıştırılır. Kullanıcı uygulamayı ziyaret ettiği zaman bundle dosyası indirilir ve kullanıcı ile etkileşime geçilir.

Bu süreçte bir hayli loading tarzı ekranlar göstermemiz gerekiyor performans açısından, SEO sorunu da cabası. Bunun önüne geçmek için SSR dediğimiz yapı kullanılıyor.

Normalde zero-configuration mantığı üzerine tasarlanmış NextJS kullanılıyor bu durumlarda fakat bu yazıda manuel bir yapı paylaşacağım.

Server.js :

bundle dosyasındaki tüm çıktılar tarayıcıya HTML olarak gönderilir. Tarayıcı HTML’i yorumlar daha sonra Javascript dosyası indirilir.

  
import React from "react";
import express from "express";
import path from "path";
import { renderToString } from "react-dom/server";
import { StaticRouter, matchPath } from "react-router-dom";
import { Provider as ReduxProvider } from "react-redux";
import Helmet from "react-helmet";
import routes from "./routes";
import Layout from "./components/Layout";
import createStore, { initializeSession } from "./store";

const app = express();

app.use( express.static( path.resolve( __dirname, "../dist" ) ) );

app.get( "/*", ( req, res ) => {
    const context = {};
    const store = createStore();

    store.dispatch( initializeSession() );

    const dataRequirements =
        routes
            .filter( route => matchPath( req.url, route ) )
            .map( route => route.component )
            .filter( comp => comp.serverFetch )
            .map( comp => store.dispatch( comp.serverFetch() ) );

    Promise.all( dataRequirements )
        .then( () => {
            const jsx = (
                <ReduxProvider store={store}>
                    <StaticRouter context={context} location={req.url}>
                        <Layout/>
                    </StaticRouter>
                </ReduxProvider>
            );
            const reactDom = renderToString( jsx );
            const reduxState = store.getState();
            const helmetData = Helmet.renderStatic();

            res.writeHead( 200, { "Content-Type": "text/html" } );
            res.end( htmlTemplate( reactDom, reduxState, helmetData ) );
        } );
} );

function htmlTemplate( reactDom, reduxState, helmetData ) {
    return `
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="utf-8">
            ${ helmetData.title.toString() }
            ${ helmetData.meta.toString() }
            <title>React SSR</title>
        </head>
        
        <body>
            <div id="app">${ reactDom }</div>
            <script>
                window.REDUX_DATA = ${ JSON.stringify( reduxState ) }
            </script>
            <script src="./app.bundle.js"></script>
        </body>
        </html>
    `;
}

app.listen( 2048 );

client.js :

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider as ReduxProvider } from "react-redux";

import Layout from "./components/Layout";
import createStore from "./store";

const store = createStore( window.REDUX_DATA );

const jsx = (
    <ReduxProvider store={store}>
        <Router>
            <Layout/>
        </Router>
    </ReduxProvider>
);

const app = document.getElementById( "app" );
ReactDOM.hydrate( jsx, app );

Redux Store :

import { createStore, combineReducers, applyMiddleware } from "redux";
import thunkMiddleware from "redux-thunk";
import { fetchCircuits } from "./api";

export const initializeSession = () => ( {
    type: "INITIALIZE_SESSION",
} );

const storeData = ( data ) => ( {
    type: "STORE_DATA",
    data,
} );

export const fetchData = () => ( dispatch ) =>
    fetchCircuits()
        .then( res => dispatch( storeData( res ) ) );

const sessionReducer = ( state = false, action ) => {
    switch (action.type) {
        case "INITIALIZE_SESSION":
            return true;
        default:
            return state;
    }
};

const dataReducer = ( state = [], action ) => {
    switch (action.type) {
        case "STORE_DATA":
            return action.data;
        default:
            return state;
    }
};

const reducer = combineReducers( {
    loggedIn: sessionReducer,
    data: dataReducer,
} );

export default ( initialState ) =>
    createStore( reducer, initialState, applyMiddleware( thunkMiddleware ) );

Tüm kodlara ve uygulamaya erişmek için :

https://github.com/emreyalvac/React-Server-Side-Rendering