Skip to content

Commit fc705b8

Browse files
Step 2: Create Simple MIT Registries for Core Package
✅ Checkpoint 2.1: Create simple ComponentRegistry - Created packages/react-on-rails/src/ComponentRegistry.ts with Map-based storage - Synchronous register() and get() methods, error for getOrWaitForComponent() - Added comprehensive unit tests (12 test cases) ✅ Checkpoint 2.2: Create simple StoreRegistry - Created packages/react-on-rails/src/StoreRegistry.ts with dual Map storage - All synchronous methods: register(), getStore(), getStoreGenerator(), etc. - Error throwing stubs for async methods (getOrWaitForStore, getOrWaitForStoreGenerator) - Updated unit tests for core implementation ✅ Checkpoint 2.3: Create simple ClientRenderer - Created packages/react-on-rails/src/ClientRenderer.ts with synchronous rendering - Based on pre-force-load clientStartup.ts implementation - Direct imports from core registries, renderComponent() and reactOnRailsComponentLoaded() - Added unit tests for basic rendering functionality All registries work independently without pro features and provide clear error messages directing users to upgrade to React on Rails Pro for advanced functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 978fbf3 commit fc705b8

File tree

7 files changed

+627
-68
lines changed

7 files changed

+627
-68
lines changed

docs/JS_PRO_PACKAGE_SEPARATION_PLAN.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,39 +93,39 @@ Based on commit `4dee1ff3cff5998a38cfa758dec041ece9986623` analysis:
9393

9494
**Checkpoint 2.1**: Create simple ComponentRegistry
9595

96-
- [ ] Create `packages/react-on-rails/src/ComponentRegistry.ts` with:
96+
- [x] Create `packages/react-on-rails/src/ComponentRegistry.ts` with:
9797
- Simple Map-based storage (`registeredComponents = new Map()`)
9898
- Synchronous `register(components)` method
9999
- Synchronous `get(name)` method with error on missing component
100100
- `components()` method returning Map
101101
- Error throwing stub for `getOrWaitForComponent()` with message: `'getOrWaitForComponent requires react-on-rails-pro package'`
102-
- [ ] Write unit tests in `packages/react-on-rails/tests/ComponentRegistry.test.js`
103-
- [ ] Verify basic functionality with tests
102+
- [x] Write unit tests in `packages/react-on-rails/tests/ComponentRegistry.test.js`
103+
- [x] Verify basic functionality with tests
104104

105105
**Checkpoint 2.2**: Create simple StoreRegistry
106106

107-
- [ ] Create `packages/react-on-rails/src/StoreRegistry.ts` with:
107+
- [x] Create `packages/react-on-rails/src/StoreRegistry.ts` with:
108108
- Simple Map-based storage for generators and hydrated stores
109109
- All existing synchronous methods: `register()`, `getStore()`, `getStoreGenerator()`, `setStore()`, `clearHydratedStores()`, `storeGenerators()`, `stores()`
110110
- Error throwing stubs for async methods: `getOrWaitForStore()`, `getOrWaitForStoreGenerator()`
111-
- [ ] Write unit tests in `packages/react-on-rails/tests/StoreRegistry.test.js`
112-
- [ ] Verify basic functionality with tests
111+
- [x] Write unit tests in `packages/react-on-rails/tests/StoreRegistry.test.js`
112+
- [x] Verify basic functionality with tests
113113

114114
**Checkpoint 2.3**: Create simple ClientRenderer
115115

116-
- [ ] Create `packages/react-on-rails/src/ClientRenderer.ts` with:
116+
- [x] Create `packages/react-on-rails/src/ClientRenderer.ts` with:
117117
- Simple synchronous rendering based on pre-force-load `clientStartup.ts` implementation
118118
- Direct imports of core registries: `import { get as getComponent } from './ComponentRegistry'`
119119
- Basic `renderComponent(domId: string)` function
120120
- Export `reactOnRailsComponentLoaded` function
121-
- [ ] Write unit tests for basic rendering
122-
- [ ] Test simple component rendering works
121+
- [x] Write unit tests for basic rendering
122+
- [x] Test simple component rendering works
123123

124124
**Success Validation**:
125125

126-
- [ ] All unit tests pass
127-
- [ ] Core registries work independently
128-
- [ ] Simple rendering works without pro features
126+
- [x] All unit tests pass
127+
- [x] Core registries work independently
128+
- [x] Simple rendering works without pro features
129129

130130
### Step 3: Update Core Package to Use New Registries
131131

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import type { ReactElement } from 'react';
2+
import type { RegisteredComponent, RailsContext } from './types/index.ts';
3+
import ComponentRegistry from './ComponentRegistry.ts';
4+
import StoreRegistry from './StoreRegistry.ts';
5+
import createReactOutput from './createReactOutput.ts';
6+
import reactHydrateOrRender from './reactHydrateOrRender.ts';
7+
import { getRailsContext } from './context.ts';
8+
import { isServerRenderHash } from './isServerRenderResult.ts';
9+
10+
const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store';
11+
12+
function initializeStore(el: Element, railsContext: RailsContext): void {
13+
const name = el.getAttribute(REACT_ON_RAILS_STORE_ATTRIBUTE) || '';
14+
const props = el.textContent !== null ? (JSON.parse(el.textContent) as Record<string, unknown>) : {};
15+
const storeGenerator = StoreRegistry.getStoreGenerator(name);
16+
const store = storeGenerator(props, railsContext);
17+
StoreRegistry.setStore(name, store);
18+
}
19+
20+
function forEachStore(railsContext: RailsContext): void {
21+
const els = document.querySelectorAll(`[${REACT_ON_RAILS_STORE_ATTRIBUTE}]`);
22+
for (let i = 0; i < els.length; i += 1) {
23+
initializeStore(els[i], railsContext);
24+
}
25+
}
26+
27+
function domNodeIdForEl(el: Element): string {
28+
return el.getAttribute('data-dom-id') || '';
29+
}
30+
31+
function delegateToRenderer(
32+
componentObj: RegisteredComponent,
33+
props: Record<string, unknown>,
34+
railsContext: RailsContext,
35+
domNodeId: string,
36+
trace: boolean,
37+
): boolean {
38+
const { name, component, isRenderer } = componentObj;
39+
40+
if (isRenderer) {
41+
if (trace) {
42+
console.log(
43+
`\
44+
DELEGATING TO RENDERER ${name} for dom node with id: ${domNodeId} with props, railsContext:`,
45+
props,
46+
railsContext,
47+
);
48+
}
49+
50+
// Call the renderer function with the expected signature
51+
(component as (props: Record<string, unknown>, railsContext: RailsContext, domNodeId: string) => void)(
52+
props,
53+
railsContext,
54+
domNodeId,
55+
);
56+
return true;
57+
}
58+
59+
return false;
60+
}
61+
62+
/**
63+
* Used for client rendering by ReactOnRails. Either calls ReactDOM.hydrate, ReactDOM.render, or
64+
* delegates to a renderer registered by the user.
65+
*/
66+
function renderElement(el: Element, railsContext: RailsContext): void {
67+
// This must match lib/react_on_rails/helper.rb
68+
const name = el.getAttribute('data-component-name') || '';
69+
const domNodeId = domNodeIdForEl(el);
70+
const props = el.textContent !== null ? (JSON.parse(el.textContent) as Record<string, unknown>) : {};
71+
const trace = el.getAttribute('data-trace') === 'true';
72+
73+
try {
74+
const domNode = document.getElementById(domNodeId);
75+
if (domNode) {
76+
const componentObj = ComponentRegistry.get(name);
77+
if (delegateToRenderer(componentObj, props, railsContext, domNodeId, trace)) {
78+
return;
79+
}
80+
81+
// Hydrate if available and was server rendered
82+
const shouldHydrate = !!domNode.innerHTML;
83+
84+
const reactElementOrRouterResult = createReactOutput({
85+
componentObj,
86+
props,
87+
domNodeId,
88+
trace,
89+
railsContext,
90+
shouldHydrate,
91+
});
92+
93+
if (isServerRenderHash(reactElementOrRouterResult)) {
94+
throw new Error(`\
95+
You returned a server side type of react-router error: ${JSON.stringify(reactElementOrRouterResult)}
96+
You should return a React.Component always for the client side entry point.`);
97+
} else {
98+
reactHydrateOrRender(domNode, reactElementOrRouterResult as ReactElement, shouldHydrate);
99+
}
100+
}
101+
} catch (e: unknown) {
102+
const error = e as Error;
103+
console.error(error.message);
104+
error.message = `ReactOnRails encountered an error while rendering component: ${name}. See above error message.`;
105+
throw error;
106+
}
107+
}
108+
109+
/**
110+
* Render a single component by its DOM ID.
111+
* This is the main entry point for rendering individual components.
112+
*/
113+
export function renderComponent(domId: string): void {
114+
const railsContext = getRailsContext();
115+
116+
// If no react on rails context
117+
if (!railsContext) return;
118+
119+
// Initialize stores first
120+
forEachStore(railsContext);
121+
122+
// Find the element with the matching data-dom-id
123+
const el = document.querySelector(`[data-dom-id="${domId}"]`);
124+
if (!el) return;
125+
126+
renderElement(el, railsContext);
127+
}
128+
129+
/**
130+
* Public API function that can be called to render a component after it has been loaded.
131+
* This is the function that should be exported and used by the Rails integration.
132+
*/
133+
export function reactOnRailsComponentLoaded(domId: string): void {
134+
renderComponent(domId);
135+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { RegisteredComponent, ReactComponentOrRenderFunction } from './types/index.ts';
2+
import isRenderFunction from './isRenderFunction.ts';
3+
4+
const registeredComponents = new Map<string, RegisteredComponent>();
5+
6+
export default {
7+
/**
8+
* @param components { component1: component1, component2: component2, etc. }
9+
*/
10+
register(components: Record<string, ReactComponentOrRenderFunction>): void {
11+
Object.keys(components).forEach((name) => {
12+
if (registeredComponents.has(name)) {
13+
console.warn('Called register for component that is already registered', name);
14+
}
15+
16+
const component = components[name];
17+
if (!component) {
18+
throw new Error(`Called register with null component named ${name}`);
19+
}
20+
21+
const renderFunction = isRenderFunction(component);
22+
const isRenderer = renderFunction && component.length === 3;
23+
24+
registeredComponents.set(name, {
25+
name,
26+
component,
27+
renderFunction,
28+
isRenderer,
29+
});
30+
});
31+
},
32+
33+
/**
34+
* @param name
35+
* @returns { name, component, renderFunction, isRenderer }
36+
*/
37+
get(name: string): RegisteredComponent {
38+
const registeredComponent = registeredComponents.get(name);
39+
if (registeredComponent !== undefined) {
40+
return registeredComponent;
41+
}
42+
43+
const keys = Array.from(registeredComponents.keys()).join(', ');
44+
throw new Error(`Could not find component registered with name ${name}. \
45+
Registered component names include [ ${keys} ]. Maybe you forgot to register the component?`);
46+
},
47+
48+
/**
49+
* Get a Map containing all registered components. Useful for debugging.
50+
* @returns Map where key is the component name and values are the
51+
* { name, component, renderFunction, isRenderer}
52+
*/
53+
components(): Map<string, RegisteredComponent> {
54+
return registeredComponents;
55+
},
56+
57+
/**
58+
* Pro-only method that waits for component registration
59+
* @param _name Component name to wait for
60+
* @throws Always throws error indicating pro package is required
61+
*/
62+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
63+
getOrWaitForComponent(_name: string): never {
64+
throw new Error('getOrWaitForComponent requires react-on-rails-pro package');
65+
},
66+
67+
/**
68+
* Clear all registered components (for testing purposes)
69+
* @private
70+
*/
71+
clear(): void {
72+
registeredComponents.clear();
73+
},
74+
};

0 commit comments

Comments
 (0)