From 0d942050d64fc91670ff3c9c7485a52abbdae484 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 12 Mar 2020 06:09:11 -0700 Subject: [PATCH] Wire up bundler configs This allows different flight server and clients to have different configs depending on bundler to serialize and resolve modules. --- .eslintrc.js | 6 +++ .../react-client/src/ReactFlightClient.js | 7 +++ .../ReactFlightClientHostConfigNoStream.js | 30 ++++++++++++ .../ReactFlightClientHostConfig.custom.js | 5 ++ ...ReactFlightClientHostConfig.dom-browser.js | 1 + .../ReactFlightClientHostConfig.dom-relay.js | 1 + .../forks/ReactFlightClientHostConfig.dom.js | 1 + .../ReactFlightDOMRelayClientHostConfig.js | 25 ++-------- .../src/ReactFlightDOMRelayServer.js | 2 +- .../ReactFlightDOMRelayServerHostConfig.js | 29 +++++++++-- .../ReactFlightDOMRelayClientIntegration.js | 25 ++++++++++ .../ReactFlightDOMRelayServerIntegration.js | 3 ++ .../ReactFlightClientWebpackBundlerConfig.js | 48 +++++++++++++++++++ .../src/ReactFlightDOMServerBrowser.js | 8 +++- .../src/ReactFlightDOMServerNode.js | 9 +++- .../ReactFlightServerWebpackBundlerConfig.js | 28 +++++++++++ .../src/ReactNoopFlightServer.js | 7 ++- .../react-server/src/ReactFlightServer.js | 12 ++++- .../ReactFlightServerBundlerConfigCustom.js | 15 ++++++ .../forks/ReactFlightServerConfig.custom.js | 1 + .../ReactFlightServerConfig.dom-browser.js | 1 + .../src/forks/ReactFlightServerConfig.dom.js | 1 + scripts/flow/environment.js | 2 + scripts/flow/react-relay-hooks.js | 19 ++++++++ scripts/jest/setupHostConfigs.js | 3 ++ scripts/rollup/bundles.js | 2 +- 26 files changed, 259 insertions(+), 32 deletions(-) create mode 100644 packages/react-client/src/ReactFlightClientHostConfigNoStream.js create mode 100644 packages/react-flight-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js create mode 100644 packages/react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js create mode 100644 packages/react-flight-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js create mode 100644 packages/react-server/src/ReactFlightServerBundlerConfigCustom.js diff --git a/.eslintrc.js b/.eslintrc.js index 112b17120f065..bf0b774eaaa96 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -157,6 +157,12 @@ module.exports = { nativeFabricUIManager: true, }, }, + { + files: ['packages/react-flight-dom-webpack/**/*.js'], + globals: { + '__webpack_chunk_load__': true, + }, + }, ], globals: { diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index ef236394e0ade..d889001cd734b 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -9,6 +9,13 @@ import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; +// import type {ModuleMetaData} from './ReactFlightClientHostConfig'; + +// import { +// requireModuleIfAvailable, +// waitForModule, +// } from './ReactFlightClientHostConfig'; + export type ReactModelRoot = {| model: T, |}; diff --git a/packages/react-client/src/ReactFlightClientHostConfigNoStream.js b/packages/react-client/src/ReactFlightClientHostConfigNoStream.js new file mode 100644 index 0000000000000..17f29a9f26c50 --- /dev/null +++ b/packages/react-client/src/ReactFlightClientHostConfigNoStream.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type StringDecoder = void; + +export const supportsBinaryStreams = false; + +export function createStringDecoder(): void { + throw new Error('Should never be called'); +} + +export function readPartialStringChunk( + decoder: StringDecoder, + buffer: Uint8Array, +): string { + throw new Error('Should never be called'); +} + +export function readFinalStringChunk( + decoder: StringDecoder, + buffer: Uint8Array, +): string { + throw new Error('Should never be called'); +} diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js index ea0037f25ff84..420abfa6637e4 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js @@ -24,6 +24,11 @@ // really an argument to a top-level wrapping function. declare var $$$hostConfig: any; + +export opaque type ModuleMetaData = mixed; // eslint-disable-line no-undef +export const requireModuleIfAvailable = $$$hostConfig.requireModuleIfAvailable; +export const waitForModule = $$$hostConfig.waitForModule; + export opaque type Source = mixed; // eslint-disable-line no-undef export opaque type StringDecoder = mixed; // eslint-disable-line no-undef diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js index eb1e6199b425d..80d967d3ecdba 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js @@ -8,3 +8,4 @@ */ export * from 'react-client/src/ReactFlightClientHostConfigBrowser'; +export * from 'react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig'; diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-relay.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-relay.js index 7428b890f2e33..d055870a6afaa 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-relay.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom-relay.js @@ -8,3 +8,4 @@ */ export * from 'react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig'; +export * from '../ReactFlightClientHostConfigNoStream'; diff --git a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom.js b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom.js index eb1e6199b425d..80d967d3ecdba 100644 --- a/packages/react-client/src/forks/ReactFlightClientHostConfig.dom.js +++ b/packages/react-client/src/forks/ReactFlightClientHostConfig.dom.js @@ -8,3 +8,4 @@ */ export * from 'react-client/src/ReactFlightClientHostConfigBrowser'; +export * from 'react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig'; diff --git a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js index 17f29a9f26c50..8e352bd0e63e9 100644 --- a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js +++ b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js @@ -7,24 +7,9 @@ * @flow */ -export type StringDecoder = void; +export { + requireModuleIfAvailable, + waitForModule, +} from 'ReactFlightDOMRelayClientIntegration'; -export const supportsBinaryStreams = false; - -export function createStringDecoder(): void { - throw new Error('Should never be called'); -} - -export function readPartialStringChunk( - decoder: StringDecoder, - buffer: Uint8Array, -): string { - throw new Error('Should never be called'); -} - -export function readFinalStringChunk( - decoder: StringDecoder, - buffer: Uint8Array, -): string { - throw new Error('Should never be called'); -} +export type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration'; diff --git a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayServer.js b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayServer.js index 9c589a9f61daa..95b52750498a7 100644 --- a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayServer.js +++ b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayServer.js @@ -13,7 +13,7 @@ import type {Destination} from './ReactFlightDOMRelayServerHostConfig'; import {createRequest, startWork} from 'react-server/src/ReactFlightServer'; function render(model: ReactModel, destination: Destination): void { - let request = createRequest(model, destination); + let request = createRequest(model, destination, undefined); startWork(request); } diff --git a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js index bd6166ca95d99..4b5fd5b33f3eb 100644 --- a/packages/react-flight-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js +++ b/packages/react-flight-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js @@ -9,13 +9,34 @@ import type {Request, ReactModel} from 'react-server/src/ReactFlightServer'; -import type {Destination} from 'ReactFlightDOMRelayServerIntegration'; +import type { + Destination, + ModuleReference, + ModuleMetaData, +} from 'ReactFlightDOMRelayServerIntegration'; import {resolveModelToJSON} from 'react-server/src/ReactFlightServer'; -import {emitModel, emitError} from 'ReactFlightDOMRelayServerIntegration'; - -export type {Destination} from 'ReactFlightDOMRelayServerIntegration'; +import { + emitModel, + emitError, + resolveResourceMetaData, +} from 'ReactFlightDOMRelayServerIntegration'; + +export type { + Destination, + ModuleReference, + ModuleMetaData, +} from 'ReactFlightDOMRelayServerIntegration'; + +export type BundlerConfig = void; + +export function resolveModuleMetaData( + config: BundlerConfig, + resource: ModuleReference, +): ModuleMetaData { + return resolveResourceMetaData(resource); +} type JSONValue = | string diff --git a/packages/react-flight-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js b/packages/react-flight-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js new file mode 100644 index 0000000000000..7246e910040f6 --- /dev/null +++ b/packages/react-flight-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +function getFakeModule() { + return function FakeModule(props, data) { + return data; + }; +} + +const ReactFlightDOMRelayClientIntegration = { + requireModuleIfAvailable(jsResource) { + return getFakeModule(); + }, + waitForModule(jsResource) { + return Promise.resolve(getFakeModule()); + }, +}; + +module.exports = ReactFlightDOMRelayClientIntegration; diff --git a/packages/react-flight-dom-relay/src/__mocks__/ReactFlightDOMRelayServerIntegration.js b/packages/react-flight-dom-relay/src/__mocks__/ReactFlightDOMRelayServerIntegration.js index 212586b30b16e..89304a070de4e 100644 --- a/packages/react-flight-dom-relay/src/__mocks__/ReactFlightDOMRelayServerIntegration.js +++ b/packages/react-flight-dom-relay/src/__mocks__/ReactFlightDOMRelayServerIntegration.js @@ -23,6 +23,9 @@ const ReactFlightDOMRelayServerIntegration = { }); }, close(destination) {}, + resolveResourceMetaData(resource) { + return resource; + }, }; module.exports = ReactFlightDOMRelayServerIntegration; diff --git a/packages/react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js b/packages/react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js new file mode 100644 index 0000000000000..8db5174f0aa47 --- /dev/null +++ b/packages/react-flight-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type ModuleMetaData = { + id: string, + chunks: Array>, +}; + +export type Thenable = { + then(resolve: () => mixed, reject: (mixed) => mixed): mixed, + ... +}; + +export function requireModuleIfAvailable( + moduleData: ModuleMetaData, +): T | null { + let entry = require.cache[moduleData.id]; + if (entry) { + return entry.exports.default; + } + // Ideally Webpack would let us inspect that all chunks have loaded and + // just call require synchronously here. Unfortunately, the chunk cache + // doesn't expose this information without creating a Promise first. + // This causes an unfortunate exponential many-pass render since each + // level in the tree will restart to initialize that module. + // This will lead to unacceptable performance in React and will need + // changes to Webpack. + return null; +} + +export function waitForModule(moduleData: ModuleMetaData): Thenable { + let chunks = moduleData.chunks[0]; // First candidate + if (chunks.length === 1) { + return __webpack_chunk_load__(chunks[0]); + } else { + let promises = []; + for (let i = 0; i < chunks.length; i++) { + promises.push(__webpack_chunk_load__(chunks[i])); + } + return Promise.all(promises); + } +} diff --git a/packages/react-flight-dom-webpack/src/ReactFlightDOMServerBrowser.js b/packages/react-flight-dom-webpack/src/ReactFlightDOMServerBrowser.js index 0d9468bbe6260..bbbf5f18ea867 100644 --- a/packages/react-flight-dom-webpack/src/ReactFlightDOMServerBrowser.js +++ b/packages/react-flight-dom-webpack/src/ReactFlightDOMServerBrowser.js @@ -8,6 +8,7 @@ */ import type {ReactModel} from 'react-server/src/ReactFlightServer'; +import type {BundlerConfig} from './ReactFlightServerWebpackBundlerConfig'; import { createRequest, @@ -15,11 +16,14 @@ import { startFlowing, } from 'react-server/src/ReactFlightServer'; -function renderToReadableStream(model: ReactModel): ReadableStream { +function renderToReadableStream( + model: ReactModel, + webpackMap: BundlerConfig, +): ReadableStream { let request; return new ReadableStream({ start(controller) { - request = createRequest(model, controller); + request = createRequest(model, controller, webpackMap); startWork(request); }, pull(controller) { diff --git a/packages/react-flight-dom-webpack/src/ReactFlightDOMServerNode.js b/packages/react-flight-dom-webpack/src/ReactFlightDOMServerNode.js index 8bc5e20d4ed55..413e42de9c1e8 100644 --- a/packages/react-flight-dom-webpack/src/ReactFlightDOMServerNode.js +++ b/packages/react-flight-dom-webpack/src/ReactFlightDOMServerNode.js @@ -8,6 +8,7 @@ */ import type {ReactModel} from 'react-server/src/ReactFlightServer'; +import type {BundlerConfig} from './ReactFlightServerWebpackBundlerConfig'; import type {Writable} from 'stream'; import { @@ -20,8 +21,12 @@ function createDrainHandler(destination, request) { return () => startFlowing(request); } -function pipeToNodeWritable(model: ReactModel, destination: Writable): void { - let request = createRequest(model, destination); +function pipeToNodeWritable( + model: ReactModel, + destination: Writable, + webpackMap: BundlerConfig, +): void { + let request = createRequest(model, destination, webpackMap); destination.on('drain', createDrainHandler(destination, request)); startWork(request); } diff --git a/packages/react-flight-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js b/packages/react-flight-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js new file mode 100644 index 0000000000000..8d37ebdc59981 --- /dev/null +++ b/packages/react-flight-dom-webpack/src/ReactFlightServerWebpackBundlerConfig.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +type WebpackMap = { + [filename: string]: ModuleMetaData, +}; + +export type BundlerConfig = WebpackMap; + +export type ModuleReference = string; + +export type ModuleMetaData = { + id: string, + chunks: Array>, +}; + +export function resolveModuleMetaData( + config: BundlerConfig, + modulePath: ModuleReference, +): ModuleMetaData { + return config[modulePath]; +} diff --git a/packages/react-noop-renderer/src/ReactNoopFlightServer.js b/packages/react-noop-renderer/src/ReactNoopFlightServer.js index 8acf4307b9ff7..e33a664e07852 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightServer.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightServer.js @@ -47,7 +47,12 @@ const ReactNoopFlightServer = ReactFlightServer({ function render(model: ReactModel): Destination { let destination: Destination = []; - let request = ReactNoopFlightServer.createRequest(model, destination); + let bundlerConfig = undefined; + let request = ReactNoopFlightServer.createRequest( + model, + destination, + bundlerConfig, + ); ReactNoopFlightServer.startWork(request); return destination; } diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 4d1ad11a3c8cf..e3df453485a00 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -7,7 +7,13 @@ * @flow */ -import type {Destination, Chunk} from './ReactFlightServerConfig'; +import type { + Destination, + Chunk, + BundlerConfig, + // ModuleReference, + // ModuleMetaData, +} from './ReactFlightServerConfig'; import { scheduleWork, @@ -18,6 +24,7 @@ import { close, processModelChunk, processErrorChunk, + // resolveModuleMetaData, } from './ReactFlightServerConfig'; import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; @@ -49,6 +56,7 @@ type Segment = { export type Request = { destination: Destination, + bundlerConfig: BundlerConfig, nextChunkId: number, pendingChunks: number, pingedSegments: Array, @@ -61,10 +69,12 @@ export type Request = { export function createRequest( model: ReactModel, destination: Destination, + bundlerConfig: BundlerConfig, ): Request { let pingedSegments = []; let request = { destination, + bundlerConfig, nextChunkId: 0, pendingChunks: 0, pingedSegments: pingedSegments, diff --git a/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js b/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js new file mode 100644 index 0000000000000..eebaf480d14a2 --- /dev/null +++ b/packages/react-server/src/ReactFlightServerBundlerConfigCustom.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +declare var $$$hostConfig: any; + +export opaque type BundlerConfig = mixed; // eslint-disable-line no-undef +export opaque type ModuleReference = mixed; // eslint-disable-line no-undef +export opaque type ModuleMetaData = mixed; // eslint-disable-line no-undef +export const resolveModuleMetaData = $$$hostConfig.resolveModuleMetaData; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.custom.js b/packages/react-server/src/forks/ReactFlightServerConfig.custom.js index 2ade60a042904..bae5a6920d56a 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.custom.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.custom.js @@ -8,3 +8,4 @@ */ export * from '../ReactFlightServerConfigStream'; +export * from '../ReactFlightServerBundlerConfigCustom'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js index 2ade60a042904..dec9a2894548b 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js @@ -8,3 +8,4 @@ */ export * from '../ReactFlightServerConfigStream'; +export * from 'react-flight-dom-webpack/src/ReactFlightServerWebpackBundlerConfig'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom.js index 2ade60a042904..dec9a2894548b 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom.js @@ -8,3 +8,4 @@ */ export * from '../ReactFlightServerConfigStream'; +export * from 'react-flight-dom-webpack/src/ReactFlightServerWebpackBundlerConfig'; diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index c6be784d35fd4..6dfdb01472730 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -66,3 +66,5 @@ declare module 'EventListener' { ... }; } + +declare function __webpack_chunk_load__(id: string): {then(() => mixed): mixed}; diff --git a/scripts/flow/react-relay-hooks.js b/scripts/flow/react-relay-hooks.js index 0b42a2572340b..bc21b6334f2cb 100644 --- a/scripts/flow/react-relay-hooks.js +++ b/scripts/flow/react-relay-hooks.js @@ -15,6 +15,11 @@ type JSONValue = | {[key: string]: JSONValue} | Array; +type Thenable = { + then(resolve: () => mixed, reject: (error: Error) => mixed): mixed, + ... +}; + declare module 'ReactFlightDOMRelayServerIntegration' { declare export opaque type Destination; declare export function emitModel( @@ -29,4 +34,18 @@ declare module 'ReactFlightDOMRelayServerIntegration' { stack: string, ): void; declare export function close(destination: Destination): void; + + declare export opaque type ModuleReference; + declare export opaque type ModuleMetaData; + declare export function resolveResourceMetaData( + resource: ModuleReference, + ): ModuleMetaData; +} + +declare module 'ReactFlightDOMRelayClientIntegration' { + declare export opaque type ModuleMetaData; + declare export function requireModuleIfAvailable( + moduleData: ModuleMetaData, + ): T | null; + declare export function waitForModule(moduleData: ModuleMetaData): Thenable; } diff --git a/scripts/jest/setupHostConfigs.js b/scripts/jest/setupHostConfigs.js index 8bde2bd51b6e4..593c215b05604 100644 --- a/scripts/jest/setupHostConfigs.js +++ b/scripts/jest/setupHostConfigs.js @@ -34,6 +34,9 @@ jest.mock('react-server/flight', () => { return config => { jest.mock(shimServerStreamConfigPath, () => config); jest.mock(shimServerFormatConfigPath, () => config); + jest.mock('react-server/src/ReactFlightServerBundlerConfigCustom', () => ({ + resolveModuleMetaData: config.resolveModuleMetaData, + })); jest.mock(shimFlightServerConfigPath, () => require.requireActual( 'react-server/src/forks/ReactFlightServerConfig.custom' diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index d8c2e55b21ed0..205f1acfbc41b 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -258,7 +258,7 @@ const bundles = [ moduleType: RENDERER, entry: 'react-flight-dom-relay', global: 'ReactFlightDOMRelayClient', - externals: ['react'], + externals: ['react', 'ReactFlightDOMRelayClientIntegration'], }, /******* React ART *******/