Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { wrapElementInStrictMode } from '../../app/strictModeSupport';
Comment thread
justin808 marked this conversation as resolved.

export default (props, _railsContext, domNodeId) => {
const reactElement = (
const reactElement = wrapElementInStrictMode(
<div>
<h1 id="manual-render">Manual Render Example</h1>
<p>If you can see this, you can register renderer functions.</p>
</div>
</div>,
);

const domNode = document.getElementById(domNodeId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import reducers from '../../app/reducers/reducersIndex';
import composeInitialState from '../../app/store/composeInitialState';

import HelloWorldContainer from '../../app/components/HelloWorldContainer';
import { wrapElementInStrictMode } from '../../app/strictModeSupport';

/*
* Export a function that takes the props and returns a ReactComponent.
Expand All @@ -37,10 +38,10 @@ export default (props, railsContext, domNodeId) => {
// Provider uses this.props.children, so we're not typical React syntax.
// This allows redux to add additional props to the HelloWorldContainer.
const renderApp = (Komponent) => {
const element = (
const element = wrapElementInStrictMode(
<Provider store={store}>
<Komponent />
</Provider>
</Provider>,
);

render(element, document.getElementById(domNodeId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ReactOnRails from 'react-on-rails/client';
import ReactDOM from 'react-dom';

import HelloWorldContainer from '../../app/components/HelloWorldContainer';
import { wrapElementInStrictMode } from '../../app/strictModeSupport';

/*
* Export a function that returns a ReactComponent, depending on a store named SharedReduxStore.
Expand All @@ -27,10 +28,10 @@ export default (props, _railsContext, domNodeId) => {
// Provider uses this.props.children, so we're not typical React syntax.
// This allows redux to add additional props to the HelloWorldContainer.
const renderApp = (Component) => {
const element = (
const element = wrapElementInStrictMode(
<Provider store={store}>
<Component />
</Provider>
</Provider>,
);
render(element, document.getElementById(domNodeId));
};
Expand Down
12 changes: 12 additions & 0 deletions react_on_rails/spec/dummy/client/app/packs/client-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ import ReactOnRails from 'react-on-rails/client';
import HelloTurboStream from '../startup/HelloTurboStream';
import ManualRenderComponent from '../startup/ManualRenderComponent';
import SharedReduxStore from '../stores/SharedReduxStore';
import { wrapRegisteredComponentsWithStrictMode } from '../strictModeSupport';

const STRICT_MODE_PATCHED = '__reactOnRailsDummyStrictModePatched';

if (!ReactOnRails[STRICT_MODE_PATCHED]) {
const originalRegister = ReactOnRails.register.bind(ReactOnRails);

// Covers this bundle and inline ERB register calls, which run after the bundle has loaded.
ReactOnRails.register = (components) =>
originalRegister(wrapRegisteredComponentsWithStrictMode(components));
Object.defineProperty(ReactOnRails, STRICT_MODE_PATCHED, { value: true });
}

ReactOnRails.setOptions({
traceTurbolinks: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from 'react';
import ReactDOMClient from 'react-dom/client';
import { wrapElementInStrictMode } from '../strictModeSupport';

export default (props, _railsContext, domNodeId) => {
const reactElement = (
const reactElement = wrapElementInStrictMode(
<div>
<h1 id="manual-render">Manual Render Example</h1>
<p>If you can see this, you can register renderer functions.</p>
</div>
</div>,
);

const domNode = document.getElementById(domNodeId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import reducers from '../reducers/reducersIndex';
import composeInitialState from '../store/composeInitialState';

import HelloWorldContainer from '../components/HelloWorldContainer';
import { wrapElementInStrictMode } from '../strictModeSupport';

/*
* Export a function that takes the props and returns a ReactComponent.
Expand Down Expand Up @@ -42,10 +43,10 @@ export default (props, railsContext, domNodeId) => {
// Provider uses this.props.children, so we're not typical React syntax.
// This allows redux to add additional props to the HelloWorldContainer.
const renderApp = (Komponent) => {
const element = (
const element = wrapElementInStrictMode(
<Provider store={store}>
<Komponent />
</Provider>
</Provider>,
);

render(document.getElementById(domNodeId), element);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ReactOnRails from 'react-on-rails/client';
import ReactDOMClient from 'react-dom/client';

import HelloWorldContainer from '../components/HelloWorldContainer';
import { wrapElementInStrictMode } from '../strictModeSupport';

/*
* Export a function that returns a ReactComponent, depending on a store named SharedReduxStore.
Expand All @@ -32,10 +33,10 @@ export default (props, _railsContext, domNodeId) => {
// Provider uses this.props.children, so we're not typical React syntax.
// This allows redux to add additional props to the HelloWorldContainer.
const renderApp = (Component) => {
const element = (
const element = wrapElementInStrictMode(
<Provider store={store}>
<Component />
</Provider>
</Provider>,
);
render(document.getElementById(domNodeId), element);
};
Expand Down
68 changes: 68 additions & 0 deletions react_on_rails/spec/dummy/client/app/strictModeSupport.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';

const wrappedFunctionComponents = new WeakMap();
const wrappedOtherComponents = new Map(); // Map, not WeakMap: string component names are valid keys.

// Mirrors the public react-on-rails/isRenderFunction convention for this dummy-only wrapper.
const isRenderFunction = (component) => {
if (typeof component !== 'function') {
return false;
}

if (component.prototype?.isReactComponent) {
return false;
}

if (component.renderFunction) {
return true;
}

return component.length >= 2;
Comment thread
justin808 marked this conversation as resolved.
Comment thread
justin808 marked this conversation as resolved.
Comment thread
justin808 marked this conversation as resolved.
Outdated
};

const createStrictModeWrapper = (Component) => {
function StrictModeWrapper(props) {
return <React.StrictMode>{React.createElement(Component, props)}</React.StrictMode>;
}

const componentName =
typeof Component === 'string'
? Component
: Component.displayName || Component.name || 'AnonymousComponent';
StrictModeWrapper.displayName = `StrictMode(${componentName})`;

return StrictModeWrapper;
};

const wrapComponentInStrictMode = (component) => {
if (typeof component === 'function') {
const cachedComponent = wrappedFunctionComponents.get(component);
if (cachedComponent) {
return cachedComponent;
}

const wrappedComponent = createStrictModeWrapper(component);
wrappedFunctionComponents.set(component, wrappedComponent);
return wrappedComponent;
}

const cachedComponent = wrappedOtherComponents.get(component);
if (cachedComponent) {
return cachedComponent;
}

const wrappedComponent = createStrictModeWrapper(component);
wrappedOtherComponents.set(component, wrappedComponent);
return wrappedComponent;
};

export const wrapElementInStrictMode = (reactElement) => <React.StrictMode>{reactElement}</React.StrictMode>;

export const wrapRegisteredComponentsWithStrictMode = (components) =>
Object.fromEntries(
Object.entries(components).map(([name, component]) => [
name,
// OSS dummy registered render functions own their root; manual renderer entries wrap explicitly.
isRenderFunction(component) ? component : wrapComponentInStrictMode(component),
Comment thread
justin808 marked this conversation as resolved.
Outdated
Comment thread
justin808 marked this conversation as resolved.
Outdated
]),
);
74 changes: 74 additions & 0 deletions react_on_rails/spec/dummy/tests/strict-mode-support.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import PropTypes from 'prop-types';
import React from 'react';

import {
wrapElementInStrictMode,
wrapRegisteredComponentsWithStrictMode,
} from '../client/app/strictModeSupport';

describe('strictModeSupport', () => {
it('wraps registered React components in StrictMode', () => {
const HelloWorld = ({ greeting }) => <div>{greeting}</div>;
HelloWorld.propTypes = {
greeting: PropTypes.string.isRequired,
};
const wrappedComponent = wrapRegisteredComponentsWithStrictMode({ HelloWorld }).HelloWorld;

expect(wrappedComponent).not.toBe(HelloWorld);

const wrappedElement = wrappedComponent({ greeting: 'hello' });
expect(wrappedElement.type).toBe(React.StrictMode);
expect(wrappedElement.props.children.type).toBe(HelloWorld);
expect(wrappedElement.props.children.props.greeting).toBe('hello');
});

it('reuses the same wrapper for repeated registrations of the same component', () => {
const HelloWorld = () => <div>Hello</div>;

const firstWrappedComponent = wrapRegisteredComponentsWithStrictMode({ HelloWorld }).HelloWorld;
const secondWrappedComponent = wrapRegisteredComponentsWithStrictMode({ HelloWorld }).HelloWorld;

expect(firstWrappedComponent).toBe(secondWrappedComponent);
});

it('wraps class components and preserves useful display names', () => {
// eslint-disable-next-line react/prefer-stateless-function
class HelloWorld extends React.Component {
render() {
return <div>Hello</div>;
}
}

const wrappedComponent = wrapRegisteredComponentsWithStrictMode({ HelloWorld }).HelloWorld;
const wrappedElement = wrappedComponent({});

expect(wrappedComponent.displayName).toBe('StrictMode(HelloWorld)');
expect(wrappedElement.type).toBe(React.StrictMode);
expect(wrappedElement.props.children.type).toBe(HelloWorld);
});

it('does not wrap render functions or renderer functions', () => {
Comment thread
justin808 marked this conversation as resolved.
Outdated
const renderFunction = (props, railsContext) => ({ props, railsContext });
const rendererFunction = (props, railsContext, domNodeId) => ({ props, railsContext, domNodeId });
const flaggedRenderFunction = () => () => <div>Hello</div>;
flaggedRenderFunction.renderFunction = true;

const wrappedComponents = wrapRegisteredComponentsWithStrictMode({
flaggedRenderFunction,
renderFunction,
rendererFunction,
});

expect(wrappedComponents.flaggedRenderFunction).toBe(flaggedRenderFunction);
expect(wrappedComponents.renderFunction).toBe(renderFunction);
expect(wrappedComponents.rendererFunction).toBe(rendererFunction);
});

it('wraps manual render trees in StrictMode', () => {
const innerElement = <div>hello</div>;
const wrappedElement = wrapElementInStrictMode(innerElement);

expect(wrappedElement.type).toBe(React.StrictMode);
expect(wrappedElement.props.children).toBe(innerElement);
});
});
Comment thread
justin808 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import { loadableReady } from '@loadable/component';
import { HelmetProvider } from '@dr.pogodin/react-helmet';

import ClientApp from './LoadableApp';
import { wrapElementInStrictMode } from '../strictModeSupport';

const App = (props, railsContext, domNodeId) => {
loadableReady(() => {
const el = document.getElementById(domNodeId);
hydrateRoot(
el,
const reactElement = wrapElementInStrictMode(
<HelmetProvider>
{React.createElement(ClientApp, { ...props, path: railsContext.pathname })}
</HelmetProvider>,
);
hydrateRoot(el, reactElement);
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from 'react';
import { ApolloProvider, ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
import { hydrateRoot } from 'react-dom/client';
import ApolloGraphQL from '../components/ApolloGraphQL';
import { wrapElementInStrictMode } from '../strictModeSupport';

export default (_props, _railsContext, domNodeId) => {
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
Expand All @@ -22,10 +23,10 @@ export default (_props, _railsContext, domNodeId) => {
ssrForceFetchDelay: 100,
});
const el = document.getElementById(domNodeId);
const App = (
const App = wrapElementInStrictMode(
<ApolloProvider client={client}>
<ApolloGraphQL />
</ApolloProvider>
</ApolloProvider>,
);
hydrateRoot(el, App);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { hydrateRoot } from 'react-dom/client';
import { setSSRCache } from '@shakacode/use-ssr-computation.runtime';
import { RailsContext } from 'react-on-rails-pro';
import ApolloGraphQL from '../components/LazyApolloGraphQL';
import { wrapElementInStrictMode } from '../strictModeSupport';

export default (_props: unknown, _railsContext: RailsContext, domNodeId: string) => {
if (!window.__SSR_COMPUTATION_CACHE) {
Expand All @@ -18,6 +19,6 @@ export default (_props: unknown, _railsContext: RailsContext, domNodeId: string)
console.log('window.__SSR_COMPUTATION_CACHE', window.__SSR_COMPUTATION_CACHE);
const ssrComputationCache = window.__SSR_COMPUTATION_CACHE;
setSSRCache(ssrComputationCache);
const App = <ApolloGraphQL />;
const App = wrapElementInStrictMode(<ApolloGraphQL />);
hydrateRoot(el, App);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import React from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
import { wrapElementInStrictMode } from '../strictModeSupport';

const hydrateOrRender = (domEl, reactEl, prerender) => {
if (prerender) {
Expand All @@ -16,11 +17,11 @@ const hydrateOrRender = (domEl, reactEl, prerender) => {
export default (props, _railsContext, domNodeId) => {
const { prerender } = props;

const reactElement = (
const reactElement = wrapElementInStrictMode(
<div>
<h1 id="manual-render">Manual Render Example</h1>
<p>If you can see this, you can register renderer functions.</p>
</div>
</div>,
);

hydrateOrRender(document.getElementById(domNodeId), reactElement, prerender);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import reducers from '../reducers/reducersIndex';
import composeInitialState from '../store/composeInitialState';

import HelloWorldContainer from '../components/HelloWorldContainer';
import { wrapElementInStrictMode } from '../strictModeSupport';

const hydrateOrRender = (domEl, reactEl, prerender) => {
if (prerender) {
Expand Down Expand Up @@ -43,10 +44,10 @@ export default (props, railsContext, domNodeId) => {

// Provider uses this.props.children, so we're not typical React syntax.
// This allows redux to add additional props to the HelloWorldContainer.
const element = (
const element = wrapElementInStrictMode(
<Provider store={store}>
<HelloWorldContainer />
</Provider>
</Provider>,
);

hydrateOrRender(document.getElementById(domNodeId), element, prerender);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ReactOnRails from 'react-on-rails-pro';
import { hydrateRoot, createRoot } from 'react-dom/client';

import HelloWorldContainer from '../components/HelloWorldContainer';
import { wrapElementInStrictMode } from '../strictModeSupport';

const hydrateOrRender = (domEl, reactEl, prerender) => {
if (prerender) {
Expand All @@ -33,10 +34,10 @@ export default (props, _railsContext, domNodeId) => {

// Provider uses this.props.children, so we're not typical React syntax.
// This allows redux to add additional props to the HelloWorldContainer.
const element = (
const element = wrapElementInStrictMode(
<Provider store={store}>
<HelloWorldContainer />
</Provider>
</Provider>,
);

hydrateOrRender(document.getElementById(domNodeId), element, prerender);
Expand Down
Loading
Loading