Skip to content
Merged
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: 4 additions & 1 deletion jest.ssr.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ module.exports = {
'packages/'
],

globalSetup: require.resolve('@react-spectrum/test-utils/src/ssrSetup'),
globalTeardown: require.resolve('@react-spectrum/test-utils/src/ssrTeardown'),

// The test environment that will be used for testing
testEnvironment: require.resolve('@react-spectrum/test-utils/src/SSREnvironment'),
testEnvironment: 'jsdom',

// The glob patterns Jest uses to detect test files
testMatch: [
Expand Down
3 changes: 3 additions & 0 deletions packages/@react-aria/ssr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @react-aria/ssr

This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details.
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,4 @@
* governing permissions and limitations under the License.
*/

const NodeEnvironment = require('jest-environment-node');
const {Worker} = require('worker_threads');

// Setup a single worker instance that's shared between test environments via a __SSR_WORKER__ global.
let worker = new Worker(__dirname + '/hydrateWorker.js');
worker.unref();

class SSREnvironment extends NodeEnvironment {
async setup() {
await super.setup();
this.global.__SSR_WORKER__ = worker;
}

async teardown() {
await super.teardown();
}
}

module.exports = SSREnvironment;
export * from './src';
29 changes: 29 additions & 0 deletions packages/@react-aria/ssr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@react-aria/ssr",
"version": "3.0.0-alpha.1",
"private": true,
"description": "Spectrum UI components in React",
"license": "Apache-2.0",
"main": "dist/main.js",
"module": "dist/module.js",
"types": "dist/types.d.ts",
"source": "src/index.ts",
"files": [
"dist",
"src"
],
"sideEffects": false,
"repository": {
"type": "git",
"url": "https://github.com/adobe/react-spectrum"
},
"dependencies": {
"@babel/runtime": "^7.6.2"
},
"peerDependencies": {
"react": "^16.8.0"
},
"publishConfig": {
"access": "public"
}
}
79 changes: 79 additions & 0 deletions packages/@react-aria/ssr/src/SSRProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import React, {ReactNode, useContext, useMemo} from 'react';

// To support SSR, the auto incrementing id counter is stored in a context. This allows
// it to be reset on every request to ensure the client and server are consistent.
// There is also a prefix counter that is used to support async loading components
// Each async boundary must be wrapped in an SSR provider, which increments the prefix
// and resets the current id counter. This ensures that async loaded components have
// consistent ids regardless of the loading order.
interface SSRContextValue {
prefix: number,
current: number
}

// Default context value to use in case there is no SSRProvider. This is fine for
// client-only apps. In order to support multiple copies of React Aria potentially
// being on the page at once, the prefix is set to a random number. SSRProvider
// will reset this to zero for consistency between server and client, so in the
// SSR case multiple copies of React Aria is not supported.
const defaultContext: SSRContextValue = {
prefix: Math.round(Math.random() * 10000000000),
current: 0
};

const SSRContext = React.createContext<SSRContextValue>(defaultContext);

interface SSRProviderProps {
children: ReactNode
}

/**
* When using SSR with React Aria, applications must be wrapped in an SSRProvider.
* This ensures that auto generated ids are consistent between the client and server
* by resetting the incremented value on each request.
*/
export function SSRProvider(props: SSRProviderProps): JSX.Element {
let cur = useContext(SSRContext);
let value: SSRContextValue = {
// If this is the first SSRProvider, set to zero, otherwise increment.
prefix: cur === defaultContext ? 0 : cur.prefix + 1,
current: 0
};

return (
<SSRContext.Provider value={value}>
{props.children}
</SSRContext.Provider>
);
}

let canUseDOM = Boolean(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);

/** @private */
export function useSSRSafeId(defaultId?: string): string {
let ctx = useContext(SSRContext);

// If we are rendering in a non-DOM environment, and there's no SSRProvider,
// provide a warning to hint to the developer to add one.
if (ctx === defaultContext && !canUseDOM) {
console.warn('When server rendering, you must wrap your application in an <SSRProvider> to ensure consistent ids are generated between the client and server.');
}

return useMemo(() => defaultId || `react-aria-${ctx.prefix}-${++ctx.current}`, [defaultId]);
}
13 changes: 13 additions & 0 deletions packages/@react-aria/ssr/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

export * from './SSRProvider';
1 change: 1 addition & 0 deletions packages/@react-aria/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
"@react-aria/ssr": "3.0.0-alpha.1",
"@react-types/shared": "^3.2.0",
"clsx": "^1.1.1"
},
Expand Down
10 changes: 3 additions & 7 deletions packages/@react-aria/utils/src/useId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,18 @@
* governing permissions and limitations under the License.
*/

import {useLayoutEffect, useMemo, useState} from 'react';
import {useLayoutEffect, useState} from 'react';
import {useSSRSafeId} from '@react-aria/ssr';

let map: Map<string, (v: string) => void> = new Map();

let id = 0;
// don't want to conflict with ids from v2, this will guarantee something unique
// plus we'll know how many instances of this module are loaded on a page if there are more than one number ;)
let randomInstanceNumber = Math.round(Math.random() * 10000000000);

/**
* If a default is not provided, generate an id.
* @param defaultId - Default component id.
*/
export function useId(defaultId?: string): string {
let [value, setValue] = useState(defaultId);
let res = useMemo(() => value || `react-aria-${randomInstanceNumber}-${++id}`, [value]);
let res = useSSRSafeId(value);
map.set(res, setValue);
return res;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-spectrum/menu/test/Menu.ssr.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('Menu SSR', function () {
it('should render without errors', async function () {
await testSSR(__filename, `
import {Menu, Item} from '../';
<Menu>
<Menu aria-label="Menu">
<Item>Left</Item>
<Item>Middle</Item>
<Item>Right</Item>
Expand Down
1 change: 1 addition & 0 deletions packages/@react-spectrum/test-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
"@react-aria/ssr": "3.0.0-alpha.1",
"resolve": "^1.17.0"
},
"peerDependencies": {
Expand Down
49 changes: 0 additions & 49 deletions packages/@react-spectrum/test-utils/src/hydrateWorker.js

This file was deleted.

5 changes: 5 additions & 0 deletions packages/@react-spectrum/test-utils/src/ssrSetup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const {Worker} = require('worker_threads');

module.exports = async () => {
global.__SSR_SERVER__ = new Worker(__dirname + '/ssrWorker.js');
};
4 changes: 4 additions & 0 deletions packages/@react-spectrum/test-utils/src/ssrTeardown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// teardown.js
module.exports = async function () {
global.__SSR_SERVER__.terminate();
};
64 changes: 64 additions & 0 deletions packages/@react-spectrum/test-utils/src/ssrWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

const http = require('http');
const identityObjectProxy = require('identity-obj-proxy');
const ignoreStyles = require('ignore-styles');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const util = require('util');

ignoreStyles.default(undefined, (module) => {
module.exports = identityObjectProxy;
});

require('@babel/register')({
extensions: ['.js', '.ts', '.tsx']
});

let {evaluate} = require('./ssrUtils');
let {SSRProvider} = require('@react-aria/ssr');

http.createServer((req, res) => {
let body = '';
req.on('data', chunk => {
body += chunk;
});

req.on('end', () => {
let parsed = JSON.parse(body);

// Capture React errors/warning and make them fail the tests.
let errors = [];
console.error = console.warn = (...messages) => {
errors.push(util.format(...messages));
};

let html;
try {
// Evaluate the code, and render the resulting JSX element to HTML.
let element = evaluate(parsed.source, parsed.filename);
html = ReactDOMServer.renderToString(React.createElement(SSRProvider, undefined, element));
} catch (err) {
errors.push(err.stack);
}

if (errors.length > 0) {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({errors}));
} else {
res.setHeader('Content-Type', 'text/html');
res.end(html);
}
});
}).listen(18235);
Loading