Skip to content

Commit a0a4c1c

Browse files
alexeyr-ci2alexeyr
andauthored
Replace context with globalThis (#1727)
* Replace context with globalThis * Simplify getRailsContext * Update changelog and release notes * Remove indirection --------- Co-authored-by: Alexey Romanov <[email protected]>
1 parent 2b692cc commit a0a4c1c

9 files changed

+57
-101
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ Changes since the last non-beta release.
3333

3434
- Deprecated `defer_generated_component_packs` configuration option. You should use `generated_component_packs_loading_strategy` instead. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
3535

36+
### Changed
37+
38+
- The global context is now accessed using `globalThis`. [PR 1727](https://github.com/shakacode/react_on_rails/pull/1727) by [alexeyr-ci2](https://github.com/alexeyr-ci2).
39+
3640
### [15.0.0-alpha.2] - 2025-03-07
3741

3842
See [Release Notes](docs/release-notes/15.0.0.md) for full details.

docs/release-notes/15.0.0.md

+5
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ Major improvements to component and store hydration:
6868
- If you were previously using `defer_generated_component_packs: false`, use `generated_component_packs_loading_strategy: :sync` instead
6969
- For optimal performance with Shakapacker ≥ 8.2.0, consider using `generated_component_packs_loading_strategy: :async`
7070

71+
### `globalThis`
72+
73+
[`globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis) is now used in code.
74+
It should be available in browsers since 2020 and in Node, but in case your environment doesn't support it, you'll need to shim it using [globalthis](https://www.npmjs.com/package/globalthis) or [core-js](https://www.npmjs.com/package/core-js).
75+
7176
## Store Dependencies for Components
7277

7378
When using Redux stores with multiple components, you need to explicitly declare store dependencies to optimize hydration. Here's how:

node_package/src/CallbackRegistry.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ItemRegistrationCallback } from './types';
22
import { onPageLoaded, onPageUnloaded } from './pageLifecycle';
3-
import { getContextAndRailsContext } from './context';
3+
import { getRailsContext } from './context';
44

55
/**
66
* Represents information about a registered item including its value,
@@ -47,7 +47,7 @@ export default class CallbackRegistry<T> {
4747
};
4848

4949
onPageLoaded(() => {
50-
const registryTimeout = getContextAndRailsContext().railsContext?.componentRegistryTimeout;
50+
const registryTimeout = getRailsContext()?.componentRegistryTimeout;
5151
if (!registryTimeout) return;
5252

5353
timeoutId = setTimeout(triggerTimeout, registryTimeout);

node_package/src/ClientSideRenderer.ts

+16-19
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import * as ReactDOM from 'react-dom';
55
import type { ReactElement } from 'react';
66
import type { RailsContext, RegisteredComponent, RenderFunction, Root } from './types';
77

8-
import { getContextAndRailsContext, resetContextAndRailsContext, type Context } from './context';
8+
import { getRailsContext, resetRailsContext } from './context';
99
import createReactOutput from './createReactOutput';
1010
import { isServerRenderHash } from './isServerRenderResult';
1111
import reactHydrateOrRender from './reactHydrateOrRender';
1212
import { supportsRootApi } from './reactApis';
1313
import { debugTurbolinks } from './turbolinksUtils';
14+
import * as StoreRegistry from './StoreRegistry';
15+
import * as ComponentRegistry from './ComponentRegistry';
1416

1517
const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store';
1618

@@ -61,23 +63,23 @@ class ComponentRenderer {
6163
const storeDependencies = el.getAttribute('data-store-dependencies');
6264
const storeDependenciesArray = storeDependencies ? (JSON.parse(storeDependencies) as string[]) : [];
6365

64-
const { context, railsContext } = getContextAndRailsContext();
65-
if (!context || !railsContext) return;
66+
const railsContext = getRailsContext();
67+
if (!railsContext) return;
6668

6769
// Wait for all store dependencies to be loaded
6870
this.renderPromise = Promise.all(
69-
storeDependenciesArray.map((storeName) => context.ReactOnRails.getOrWaitForStore(storeName)),
71+
storeDependenciesArray.map((storeName) => StoreRegistry.getOrWaitForStore(storeName)),
7072
).then(() => {
7173
if (this.state === 'unmounted') return Promise.resolve();
72-
return this.render(el, context, railsContext);
74+
return this.render(el, railsContext);
7375
});
7476
}
7577

7678
/**
7779
* Used for client rendering by ReactOnRails. Either calls ReactDOM.hydrate, ReactDOM.render, or
7880
* delegates to a renderer registered by the user.
7981
*/
80-
private async render(el: Element, context: Context, railsContext: RailsContext): Promise<void> {
82+
private async render(el: Element, railsContext: RailsContext): Promise<void> {
8183
// This must match lib/react_on_rails/helper.rb
8284
const name = el.getAttribute('data-component-name') || '';
8385
const { domNodeId } = this;
@@ -87,7 +89,7 @@ class ComponentRenderer {
8789
try {
8890
const domNode = document.getElementById(domNodeId);
8991
if (domNode) {
90-
const componentObj = await context.ReactOnRails.getOrWaitForComponent(name);
92+
const componentObj = await ComponentRegistry.getOrWaitForComponent(name);
9193
if (this.state === 'unmounted') {
9294
return;
9395
}
@@ -181,8 +183,8 @@ class StoreRenderer {
181183

182184
constructor(storeDataElement: Element) {
183185
this.state = 'hydrating';
184-
const { context, railsContext } = getContextAndRailsContext();
185-
if (!context || !railsContext) {
186+
const railsContext = getRailsContext();
187+
if (!railsContext) {
186188
return;
187189
}
188190

@@ -191,22 +193,17 @@ class StoreRenderer {
191193
storeDataElement.textContent !== null
192194
? (JSON.parse(storeDataElement.textContent) as Record<string, unknown>)
193195
: {};
194-
this.hydratePromise = this.hydrate(context, railsContext, name, props);
196+
this.hydratePromise = this.hydrate(railsContext, name, props);
195197
}
196198

197-
private async hydrate(
198-
context: Context,
199-
railsContext: RailsContext,
200-
name: string,
201-
props: Record<string, unknown>,
202-
) {
203-
const storeGenerator = await context.ReactOnRails.getOrWaitForStoreGenerator(name);
199+
private async hydrate(railsContext: RailsContext, name: string, props: Record<string, unknown>) {
200+
const storeGenerator = await StoreRegistry.getOrWaitForStoreGenerator(name);
204201
if (this.state === 'unmounted') {
205202
return;
206203
}
207204

208205
const store = storeGenerator(props, railsContext);
209-
context.ReactOnRails.setStore(name, store);
206+
StoreRegistry.setStore(name, store);
210207
this.state = 'hydrated';
211208
}
212209

@@ -252,7 +249,7 @@ export const renderOrHydrateAllComponents = () =>
252249
function unmountAllComponents(): void {
253250
renderedRoots.forEach((root) => root.unmount());
254251
renderedRoots.clear();
255-
resetContextAndRailsContext();
252+
resetRailsContext();
256253
}
257254

258255
const storeRenderers = new Map<string, StoreRenderer>();

node_package/src/ReactOnRails.client.ts

+6-15
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import * as StoreRegistry from './StoreRegistry';
66
import buildConsoleReplay from './buildConsoleReplay';
77
import createReactOutput from './createReactOutput';
88
import * as Authenticity from './Authenticity';
9-
import context from './context';
109
import type {
1110
RegisteredComponent,
1211
RenderResult,
@@ -19,27 +18,19 @@ import type {
1918
} from './types';
2019
import reactHydrateOrRender from './reactHydrateOrRender';
2120

22-
const ctx = context();
23-
24-
if (ctx === undefined) {
25-
throw new Error("The context (usually Window or NodeJS's Global) is undefined.");
26-
}
27-
28-
if (ctx.ReactOnRails !== undefined) {
29-
/* eslint-disable @typescript-eslint/no-base-to-string -- Window and Global both have useful toString() */
21+
if (globalThis.ReactOnRails !== undefined) {
3022
throw new Error(`\
31-
The ReactOnRails value exists in the ${ctx} scope, it may not be safe to overwrite it.
23+
The ReactOnRails value exists in the ${globalThis} scope, it may not be safe to overwrite it.
3224
This could be caused by setting Webpack's optimization.runtimeChunk to "true" or "multiple," rather than "single."
3325
Check your Webpack configuration. Read more at https://github.com/shakacode/react_on_rails/issues/1558.`);
34-
/* eslint-enable @typescript-eslint/no-base-to-string */
3526
}
3627

3728
const DEFAULT_OPTIONS = {
3829
traceTurbolinks: false,
3930
turbo: false,
4031
};
4132

42-
ctx.ReactOnRails = {
33+
globalThis.ReactOnRails = {
4334
options: {},
4435

4536
register(components: Record<string, ReactComponentOrRenderFunction>): void {
@@ -199,9 +190,9 @@ ctx.ReactOnRails = {
199190
},
200191
};
201192

202-
ctx.ReactOnRails.resetOptions();
193+
globalThis.ReactOnRails.resetOptions();
203194

204-
ClientStartup.clientStartup(ctx);
195+
ClientStartup.clientStartup();
205196

206197
export * from './types';
207-
export default ctx.ReactOnRails;
198+
export default globalThis.ReactOnRails;

node_package/src/clientStartup.ts

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { type Context, isWindow } from './context';
21
import {
3-
renderOrHydrateForceLoadedComponents,
4-
renderOrHydrateAllComponents,
5-
hydrateForceLoadedStores,
62
hydrateAllStores,
3+
hydrateForceLoadedStores,
4+
renderOrHydrateAllComponents,
5+
renderOrHydrateForceLoadedComponents,
76
unmountAll,
87
} from './ClientSideRenderer';
98
import { onPageLoaded, onPageUnloaded } from './pageLifecycle';
@@ -19,20 +18,20 @@ function reactOnRailsPageUnloaded(): void {
1918
unmountAll();
2019
}
2120

22-
export function clientStartup(context: Context) {
21+
export function clientStartup() {
2322
// Check if server rendering
24-
if (!isWindow(context)) {
23+
if (globalThis.document === undefined) {
2524
return;
2625
}
2726

2827
// Tried with a file local variable, but the install handler gets called twice.
2928
// eslint-disable-next-line no-underscore-dangle
30-
if (context.__REACT_ON_RAILS_EVENT_HANDLERS_RAN_ONCE__) {
29+
if (globalThis.__REACT_ON_RAILS_EVENT_HANDLERS_RAN_ONCE__) {
3130
return;
3231
}
3332

3433
// eslint-disable-next-line no-underscore-dangle
35-
context.__REACT_ON_RAILS_EVENT_HANDLERS_RAN_ONCE__ = true;
34+
globalThis.__REACT_ON_RAILS_EVENT_HANDLERS_RAN_ONCE__ = true;
3635

3736
// Force loaded components and stores are rendered and hydrated immediately.
3837
// The hydration process can handle the concurrent hydration of components and stores,

node_package/src/context.ts

+14-47
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,36 @@
1-
import type { ReactOnRailsInternal as ReactOnRailsType, RailsContext } from './types';
1+
import type { ReactOnRailsInternal, RailsContext } from './types';
22

33
declare global {
4-
interface Window {
5-
ReactOnRails: ReactOnRailsType;
6-
__REACT_ON_RAILS_EVENT_HANDLERS_RAN_ONCE__?: boolean;
7-
}
8-
9-
namespace globalThis {
10-
/* eslint-disable no-var,vars-on-top */
11-
var ReactOnRails: ReactOnRailsType;
12-
/* eslint-enable no-var,vars-on-top */
13-
}
4+
/* eslint-disable no-var,vars-on-top,no-underscore-dangle */
5+
var ReactOnRails: ReactOnRailsInternal;
6+
var __REACT_ON_RAILS_EVENT_HANDLERS_RAN_ONCE__: boolean;
7+
/* eslint-enable no-var,vars-on-top,no-underscore-dangle */
148
}
159

16-
export type Context = Window | typeof globalThis;
17-
18-
/**
19-
* Get the context, be it window or global
20-
*/
21-
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
22-
export default function context(this: void): Context | void {
23-
return (typeof window !== 'undefined' && window) || (typeof global !== 'undefined' && global) || this;
24-
}
25-
26-
export function isWindow(ctx: Context): ctx is Window {
27-
return (ctx as Window).document !== undefined;
28-
}
29-
30-
export function reactOnRailsContext(): Context {
31-
const ctx = context();
32-
if (ctx === undefined || typeof ctx.ReactOnRails === 'undefined') {
33-
throw new Error('ReactOnRails is undefined in both global and window namespaces.');
34-
}
35-
return ctx;
36-
}
37-
38-
let currentContext: Context | null = null;
3910
let currentRailsContext: RailsContext | null = null;
4011

4112
// caches context and railsContext to avoid re-parsing rails-context each time a component is rendered
42-
// Cached values will be reset when resetContextAndRailsContext() is called
43-
export function getContextAndRailsContext(): { context: Context | null; railsContext: RailsContext | null } {
13+
// Cached values will be reset when resetRailsContext() is called
14+
export function getRailsContext(): RailsContext | null {
4415
// Return cached values if already set
45-
if (currentContext && currentRailsContext) {
46-
return { context: currentContext, railsContext: currentRailsContext };
16+
if (currentRailsContext) {
17+
return currentRailsContext;
4718
}
4819

49-
currentContext = reactOnRailsContext();
50-
5120
const el = document.getElementById('js-react-on-rails-context');
52-
if (!el || !el.textContent) {
53-
return { context: null, railsContext: null };
21+
if (!el?.textContent) {
22+
return null;
5423
}
5524

5625
try {
5726
currentRailsContext = JSON.parse(el.textContent) as RailsContext;
27+
return currentRailsContext;
5828
} catch (e) {
5929
console.error('Error parsing Rails context:', e);
60-
return { context: null, railsContext: null };
30+
return null;
6131
}
62-
63-
return { context: currentContext, railsContext: currentRailsContext };
6432
}
6533

66-
export function resetContextAndRailsContext(): void {
67-
currentContext = null;
34+
export function resetRailsContext(): void {
6835
currentRailsContext = null;
6936
}

node_package/src/turbolinksUtils.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { reactOnRailsContext } from './context';
2-
31
declare global {
42
namespace Turbolinks {
53
interface TurbolinksStatic {
@@ -18,8 +16,7 @@ export function debugTurbolinks(...msg: unknown[]): void {
1816
return;
1917
}
2018

21-
const context = reactOnRailsContext();
22-
if (context.ReactOnRails?.option('traceTurbolinks')) {
19+
if (globalThis.ReactOnRails?.option('traceTurbolinks')) {
2320
console.log('TURBO:', ...msg);
2421
}
2522
}
@@ -29,11 +26,7 @@ export function turbolinksInstalled(): boolean {
2926
}
3027

3128
export function turboInstalled() {
32-
const context = reactOnRailsContext();
33-
if (context.ReactOnRails) {
34-
return context.ReactOnRails.option('turbo') === true;
35-
}
36-
return false;
29+
return globalThis.ReactOnRails?.option('turbo') === true;
3730
}
3831

3932
export function turbolinksVersion5(): boolean {

node_package/tests/ComponentRegistry.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jest.mock('../src/pageLifecycle', () => ({
2323
}));
2424

2525
jest.mock('../src/context', () => ({
26-
getContextAndRailsContext: () => ({ railsContext: { componentRegistryTimeout: 100 } }),
26+
getRailsContext: () => ({ componentRegistryTimeout: 100 }),
2727
}));
2828

2929
describe('ComponentRegistry', () => {

0 commit comments

Comments
 (0)