Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce attw and drop React 17 support #1698

Draft
wants to merge 17 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,16 @@ overrides:
- eslint:recommended
- plugin:@typescript-eslint/eslint-recommended
- plugin:@typescript-eslint/recommended
- plugin:require-extensions/recommended
plugins:
- "@typescript-eslint"
- "require-extensions"
rules:
"@typescript-eslint/no-namespace": 0
"@typescript-eslint/no-shadow": ["error"]
# The following imports conflict with "require-extensions". TS will handle import errors.
"import/no-unresolved": 0
"import/extensions": 0

rules:
no-shadow: 0
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ yalc.lock

# IDE
.idea/
.vscode/

# TypeScript
*.tsbuildinfo
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ Please follow the recommendations outlined at [keepachangelog.com](http://keepac
### [Unreleased]
Changes since the last non-beta release.

#### Breaking
- Reduced bundle size [PR 1697](https://github.com/shakacode/react_on_rails/pull/1697) by [Romex91](https://github.com/Romex91)
- Migrated from CJS to ESM for more compact modules (~1KB improvement). **Breaking change:** Dropped CJS support. All projects running `require('react-on-rails')` will need to update to ESM `import ReactOnRails from 'react-on-rails'`.
- Add export option 'react-on-rails/client' to avoid shipping server-rendering code to browsers (~14KB improvement).

#### Fixed
- Fix obscure errors by introducing FULL_TEXT_ERRORS [PR 1695](https://github.com/shakacode/react_on_rails/pull/1695) by [Romex91](https://github.com/Romex91).

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* After updating code via Git, to prepare all examples:
```sh
cd react_on_rails/
bundle && yarn && rake examples:gen_all && rake node_package && rake
bundle && yarn && rake shakapacker_examples:gen_all && rake node_package && rake
```

See [Dev Initial Setup](#dev-initial-setup) below for, well... initial setup,
Expand Down
5 changes: 4 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
module.exports = {
export default {
preset: 'ts-jest/presets/js-with-ts',
testEnvironment: 'jsdom',
setupFiles: ['<rootDir>/node_package/tests/jest.setup.js'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
};
2 changes: 1 addition & 1 deletion node_package/src/Authenticity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AuthenticityHeaders } from './types/index';
import type { AuthenticityHeaders } from './types/index.js';

export default {
authenticityToken(): string | null {
Expand Down
4 changes: 2 additions & 2 deletions node_package/src/ComponentRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { RegisteredComponent, ReactComponentOrRenderFunction, RenderFunction } from './types/index';
import isRenderFunction from './isRenderFunction';
import type { RegisteredComponent, ReactComponentOrRenderFunction, RenderFunction } from './types/index.js';
import isRenderFunction from './isRenderFunction.js';

const registeredComponents = new Map<string, RegisteredComponent>();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import type { ReactElement } from 'react';

import * as ClientStartup from './clientStartup';
import handleError from './handleError';
import ComponentRegistry from './ComponentRegistry';
import StoreRegistry from './StoreRegistry';
import serverRenderReactComponent from './serverRenderReactComponent';
import buildConsoleReplay from './buildConsoleReplay';
import createReactOutput from './createReactOutput';
import Authenticity from './Authenticity';
import context from './context';
import * as ClientStartup from './clientStartup.js';
import ComponentRegistry from './ComponentRegistry.js';
import StoreRegistry from './StoreRegistry.js';
import buildConsoleReplay from './buildConsoleReplay.js';
import createReactOutput from './createReactOutput.js';
import Authenticity from './Authenticity.js';
import context from './context.js';
import type {
RegisteredComponent,
RenderParams,
RenderResult,
RenderReturnType,
ErrorOptions,
ReactComponentOrRenderFunction,
AuthenticityHeaders,
Store,
StoreGenerator,
} from './types';
import reactHydrateOrRender from './reactHydrateOrRender';
} from './types/index.js';
import reactHydrateOrRender from './reactHydrateOrRender.js';

const ctx = context();

Expand Down Expand Up @@ -243,8 +239,8 @@ ctx.ReactOnRails = {
* Used by server rendering by Rails
* @param options
*/
serverRenderReactComponent(options: RenderParams): null | string | Promise<RenderResult> {
return serverRenderReactComponent(options);
serverRenderReactComponent(): null | string | Promise<RenderResult> {
throw new Error('serverRenderReactComponent is not available in "react-on-rails/client". Import "react-on-rails" server-side.');
},

/**
Expand All @@ -259,8 +255,8 @@ ctx.ReactOnRails = {
* Used by Rails to catch errors in rendering
* @param options
*/
handleError(options: ErrorOptions): string | undefined {
return handleError(options);
handleError(): string | undefined {
throw new Error('handleError is not available in "react-on-rails/client". Import "react-on-rails" server-side.');
},

/**
Expand Down Expand Up @@ -303,5 +299,5 @@ ctx.ReactOnRails.resetOptions();

ClientStartup.clientStartup(ctx);

export * from "./types";
export * from "./types/index.js";
export default ctx.ReactOnRails;
28 changes: 28 additions & 0 deletions node_package/src/ReactOnRails.full.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import handleError from './handleError.js';
import serverRenderReactComponent from './serverRenderReactComponent.js';
import type {
RenderParams,
RenderResult,
ErrorOptions,
} from './types/index.js';

import Client from './ReactOnRails.client.js';

if (typeof window !== 'undefined') {
console.log('Optimization opportunity: "react-on-rails" includes ~14KB of server-rendering code. Browsers may not need it. See https://forum.shakacode.com/t/how-to-use-different-versions-of-a-file-for-client-and-server-rendering/1352 (Requires creating a free account)');
}

/**
* Used by Rails to catch errors in rendering
* @param options
*/
Client.handleError = (options: ErrorOptions): string | undefined => handleError(options);

/**
* Used by server rendering by Rails
* @param options
*/
Client.serverRenderReactComponent = (options: RenderParams): null | string | Promise<RenderResult> => serverRenderReactComponent(options);

export * from "./types/index.js";
export default Client;
8 changes: 4 additions & 4 deletions node_package/src/ReactOnRails.node.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ReactOnRails from './ReactOnRails';
import streamServerRenderedReactComponent from './streamServerRenderedReactComponent';
import ReactOnRails from './ReactOnRails.full.js';
import streamServerRenderedReactComponent from './streamServerRenderedReactComponent.js';

ReactOnRails.streamServerRenderedReactComponent = streamServerRenderedReactComponent;

export * from './ReactOnRails';
export { default } from './ReactOnRails';
export * from './ReactOnRails.full.js';
export { default } from './ReactOnRails.full.js';
2 changes: 1 addition & 1 deletion node_package/src/StoreRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Store, StoreGenerator } from './types';
import type { Store, StoreGenerator } from './types/index.js';

const registeredStoreGenerators = new Map<string, StoreGenerator>();
const hydratedStores = new Map<string, Store>();
Expand Down
4 changes: 2 additions & 2 deletions node_package/src/buildConsoleReplay.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import RenderUtils from './RenderUtils';
import scriptSanitizedVal from './scriptSanitizedVal';
import RenderUtils from './RenderUtils.js';
import scriptSanitizedVal from './scriptSanitizedVal.js';

declare global {
interface Console {
Expand Down
54 changes: 13 additions & 41 deletions node_package/src/clientStartup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ import type {
RegisteredComponent,
RenderFunction,
Root,
} from './types';
import type { Context } from './context';
} from './types/index.js';
import type { Context } from './context.js';

import createReactOutput from './createReactOutput';
import { isServerRenderHash } from './isServerRenderResult';
import reactHydrateOrRender from './reactHydrateOrRender';
import { supportsRootApi } from './reactApis';
import createReactOutput from './createReactOutput.js';
import { isServerRenderHash } from './isServerRenderResult.js';
import reactHydrateOrRender from './reactHydrateOrRender.js';

/* eslint-disable @typescript-eslint/no-explicit-any */

Expand Down Expand Up @@ -168,9 +167,7 @@ You returned a server side type of react-router error: ${JSON.stringify(reactEle
You should return a React.Component always for the client side entry point.`);
} else {
const rootOrElement = reactHydrateOrRender(domNode, reactElementOrRouterResult as ReactElement, shouldHydrate);
if (supportsRootApi) {
context.roots.push(rootOrElement as Root);
}
context.roots.push(rootOrElement as Root);
}
}
} catch (e: any) {
Expand Down Expand Up @@ -211,9 +208,7 @@ export function reactOnRailsPageLoaded(): void {
if (!railsContext) return;

const context = findContext();
if (supportsRootApi) {
context.roots = [];
}
context.roots = [];
forEachStore(context, railsContext);
forEachReactOnRailsComponentRender(context, railsContext);
}
Expand All @@ -227,46 +222,23 @@ export function reactOnRailsComponentLoaded(domId: string): void {
if (!railsContext) return;

const context = findContext();
if (supportsRootApi) {
context.roots = [];
}
context.roots = [];

const el = document.querySelector(`[data-dom-id=${domId}]`);
if (!el) return;

render(el, context, railsContext);
}

function unmount(el: Element): void {
const domNodeId = domNodeIdForEl(el);
const domNode = document.getElementById(domNodeId);
if (domNode === null) {
return;
}
try {
ReactDOM.unmountComponentAtNode(domNode);
} catch (e: any) {
console.info(`Caught error calling unmountComponentAtNode: ${e.message} for domNode`,
domNode, e);
}
}

function reactOnRailsPageUnloaded(): void {
debugTurbolinks('reactOnRailsPageUnloaded');
if (supportsRootApi) {
const { roots } = findContext();
const { roots } = findContext();

// If no react on rails components
if (!roots) return;
// If no react on rails components
if (!roots) return;

for (const root of roots) {
root.unmount();
}
} else {
const els = reactOnRailsHtmlElements();
for (let i = 0; i < els.length; i += 1) {
unmount(els[i]);
}
for (const root of roots) {
root.unmount();
}
}

Expand Down
2 changes: 1 addition & 1 deletion node_package/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ export type Context = Window | typeof globalThis;
export default function context(this: void): Context | void {
return ((typeof window !== 'undefined') && window) ||
((typeof global !== 'undefined') && global) ||
this;
globalThis;
}
4 changes: 2 additions & 2 deletions node_package/src/createReactOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import React from 'react';
import type { ServerRenderResult,
CreateParams, ReactComponent, RenderFunction, CreateReactOutputResult } from './types/index';
import {isServerRenderHash, isPromise} from "./isServerRenderResult";
CreateParams, ReactComponent, RenderFunction, CreateReactOutputResult } from './types/index.js';
import {isServerRenderHash, isPromise} from "./isServerRenderResult.js";

/**
* Logic to either call the renderFunction or call React.createElement to get the
Expand Down
2 changes: 1 addition & 1 deletion node_package/src/handleError.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import type { ErrorOptions } from './types/index';
import type { ErrorOptions } from './types/index.js';

function handleRenderFunctionIssue(options: ErrorOptions): string {
const { e, name } = options;
Expand Down
2 changes: 1 addition & 1 deletion node_package/src/isRenderFunction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// See discussion:
// https://discuss.reactjs.org/t/how-to-determine-if-js-object-is-react-component/2825/2
import { ReactComponentOrRenderFunction, RenderFunction } from "./types/index";
import { ReactComponentOrRenderFunction, RenderFunction } from "./types/index.js";

/**
* Used to determine we'll call be calling React.createElement on the component of if this is a
Expand Down
2 changes: 1 addition & 1 deletion node_package/src/isServerRenderResult.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CreateReactOutputResult, ServerRenderResult } from './types/index';
import type { CreateReactOutputResult, ServerRenderResult } from './types/index.js';

export function isServerRenderHash(testValue: CreateReactOutputResult):
testValue is ServerRenderResult {
Expand Down
8 changes: 0 additions & 8 deletions node_package/src/reactApis.ts

This file was deleted.

38 changes: 6 additions & 32 deletions node_package/src/reactHydrateOrRender.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,15 @@
import type { ReactElement } from 'react';
import ReactDOM from 'react-dom';
import type { RenderReturnType } from './types';
import { supportsRootApi } from './reactApis';
import ReactDomClient from 'react-dom/client';
import type { RenderReturnType } from './types/index.js';

type HydrateOrRenderType = (domNode: Element, reactElement: ReactElement) => RenderReturnType;

// TODO: once React dependency is updated to >= 18, we can remove this and just
// import ReactDOM from 'react-dom/client';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let reactDomClient: any;
if (supportsRootApi) {
// This will never throw an exception, but it's the way to tell Webpack the dependency is optional
// https://github.com/webpack/webpack/issues/339#issuecomment-47739112
// Unfortunately, it only converts the error to a warning.
try {
// eslint-disable-next-line global-require,import/no-unresolved
reactDomClient = require('react-dom/client');
} catch (e) {
// We should never get here, but if we do, we'll just use the default ReactDOM
// and live with the warning.
reactDomClient = ReactDOM;
}
}

const reactHydrate: HydrateOrRenderType = supportsRootApi ?
reactDomClient.hydrateRoot :
(domNode, reactElement) => ReactDOM.hydrate(reactElement, domNode);
const reactHydrate: HydrateOrRenderType = ReactDomClient.hydrateRoot;

function reactRender(domNode: Element, reactElement: ReactElement): RenderReturnType {
if (supportsRootApi) {
const root = reactDomClient.createRoot(domNode);
root.render(reactElement);
return root;
}

// eslint-disable-next-line react/no-render-return-value
return ReactDOM.render(reactElement, domNode);
const root = ReactDomClient.createRoot(domNode);
root.render(reactElement);
return root;
}

export default function reactHydrateOrRender(domNode: Element, reactElement: ReactElement, hydrate: boolean): RenderReturnType {
Expand Down
Loading
Loading