• Author: @ heineiuo
  • Release time: 2021-03-23

TL; DR

This article has nothing to do with SSR, nor with the React Server Component. The React backend framework is literally used to develop the Node.js backend, decouple and manage dependencies based on React, control component lifecycle, and intuitively define server state using JSX.

The starting point

With the complexity of the back end, dependency management mechanisms inevitably need to be introduced, where classic design patterns from OOP languages such as the factory pattern and the IoC pattern come in handy. On the one hand, it is decoupled; on the other hand, it can improve development efficiency, freeing it from endless new and passing parameters.

The Node.js ecosystem already has some IoC implementations that use the decorators syntax, such as InversifyJS(Full Screen Java flavor).

So is it possible to implement IoC without decorators? Angular uses a Java DI-like schema. React doesn’t use a Java DI-like schema at all. Why does it feel like elegant dependency management?

const Theme = createContext({})
const Router = createContext({})

// Provider.js
function Provider (props){
  return (
    <Theme.Provider value={{ primaryColor: "#1d7dfa}} ">
        <Router.Provider value={{ currentPathname: "/github}} ">
          {props.children}
        </Router.Provider>
    </Theme.Provider>)}// Consumer.js
function Consumer (){
  const theme = useContext(Theme)
  const router = useContext(Router)
}
Copy the code

The Context API separates the Provider from the Consumer, using a hook (useContext) to “inject” the Provider’s value into the Consumer. The Consumer does not get the Provider’s value when creating the Provider instance internally, which… It’s inversion of control…

The next step

However, React is known as a UI framework that uses the virtual DOM to implement the interface. React JSX uses div and SPAN keywords that correspond to page elements. In recent years, with the development of SSR, there are many developers in node.js server directly using ReactDOMServer to render HTML, instead of the traditional template…. Since the purpose of this article is to implement the back end, there is no point in just doing AN SSR, at least to render JSON.

Given the declarative JSX syntax, declaring a JSON in JSX is the most straightforward choice. This brings me to GraphQL, which, by defining schemas and resolvers, outputs a fairly free JSON.

So…

Why don’t I just use GraphQL?

On second thought, if I wanted to declare a JSON using JSX, it would not achieve the purpose of being a backend framework. Besides JSON, other responses (such as text and arrayBuffer) would not be easily declared using JSX.

How about switching gears and declaring a ServerState instead? The different components on the server are nested by JSX and together constitute a server state. Nested relationships are dependencies, and requests and responses can be received and processed by a single component abstracted from the Server.


<ConfigProvider>
    <DatabaseProvider>
        <ThirdPartyService>
            <SessionProvider>
                <Server onRequest={handleRequest}>
            </SessionProvider>
        </ThirdPartyService>
    </DatabaseProvider>
</ConfigProvider>
Copy the code

Dig MAO began

In fact, with the above structure, Coding is very smooth, and providers are written in a fixed mode:

// Database.tsx
import { FC, useState, useContext, createContext } from "react";
import { ConfigContext } from "./ConfigContext";

class MockDatabaseClient {
  options: any;
  constructor(options: any) {
    this.options = options;
  }

  async query<T = any> () :Promise<T> {
    const result = {
      count: Math.random() < 0.5 ? 1 : 2,}as unknown;
    return result asT; }}type DatabaseState = {
  db: MockDatabaseClient;
};

export const DatabaseContext = createContext({} as DatabaseState);

/** * The child component gets the DB from useContext(DatabaseContext). * /
export const DatabaseProvider: FC = (props) = > {
  const config = useContext(ConfigContext);
  const [db] = useState(new MockDatabaseClient(config.dbOptions));
  return (
    <DatabaseContext.Provider value={{ db}} >
      {props.children}
    </DatabaseContext.Provider>
  );
};


Copy the code

Server is a little bit more special

// Server.tsx
import http from "http";
import { FC, useContext, useEffect, useRef } from "react";
import { ConfigContext } from "./ConfigContext";
import { DatabaseContext } from "./DatabaseContext";

/** * Won't update when `port` change. * Change `key` prop to close/update server. */
export const Server: FC = (props) = > {
  const ref = useRef<any> ();const config = useContext(ConfigContext);
  const { db } = useContext(DatabaseContext);
  const refPort = useRef<number>(config.port);

  useEffect(() = > {
    ref.current = async (event: any) = > {const result = await db.query<{ count: number} > (); event.response.end(` <! DOCTYPE html><meta charset="utf8"/>helloThe ${Array.from({
          length: result.count,
        })
          .fill("🐈")
          .join("")} `
      );
    };
  }, [db]);

  useEffect(() = > {
    const httpServer = http.createServer((req, res) = > {
      if (ref.current) {
        ref.current({
          request: req,
          response: res, }); }}); httpServer.listen(refPort.current,() = > {
      console.log(`HTTP server listening on port ${refPort.current}`);
    });

    return() :void= >{ httpServer.close(); }; } []);return null;
};

Copy the code

The last step

So, how do you run?

It is a good idea to implement a CustomRenderer with the React-Reconciler, but given that the entire tree ultimately returns only a NULL, it can be implemented in a simple way first:


import ReactDOM from 'react-dom'
import { JSDOM, DOMWindow } from 'jsdom'
import { App } from './App'

// make ts happy
declare const global: NodeJS.Global & { window: DOMWindow; document: Document }

const dom = new JSDOM(`<div id="app"></div>`)
/** * ReactDOM accesses the window object, so register globally. * /
global.window = dom.window
global.document = window.document

/** * what is the reason for using #app instead of document.body? * /
ReactDOM.render(<App></App>.document.querySelector('#app'))

Copy the code

(Jsdom is a good thing.)

Effect:

This code can be obtained directly from heineiuo/react-as-backend-framework.

feeling

React isn’t a complete UI framework in many people’s eyes. It’s not even a UI framework in my eyes. It’s a “state declaration framework.” A tree formed by all JSX is just a state. This state may be rendered to DOM or Native, or you may have nothing to do with the UI. For example, the above code consists almost entirely of React Context and only returns a null.

Learn Once, Write Anywhere

This time only implemented a Hello World, reflecting the React based IoC mechanism, but also lacked some common components, such as routing. The next article implements routing.