diff --git a/Forte.Web.React/Configuration/ReactConfiguration.cs b/Forte.Web.React/Configuration/ReactConfiguration.cs index 50fd912..9789015 100644 --- a/Forte.Web.React/Configuration/ReactConfiguration.cs +++ b/Forte.Web.React/Configuration/ReactConfiguration.cs @@ -1,14 +1,49 @@ using System; using System.Collections.Generic; +using Forte.Web.React.React; namespace Forte.Web.React.Configuration; public class ReactConfiguration { + /// + /// Collection of URLs pointing to scripts. + /// public List ScriptUrls { get; set; } = new(); + + /// + /// Indicates whether server-side rendering is globally disabled. Default value is "false". + /// public bool IsServerSideDisabled { get; set; } = false; + + /// + /// Version of React being used. + /// public Version ReactVersion { get; set; } = null!; + + /// + /// Name of the object used to save properties. Default value is "__reactProps". + /// public string NameOfObjectToSaveProps { get; set; } = "__reactProps"; + + /// + /// Indicates whether caching is used. Default value is "true". + /// + /// This property specifically controls the usage of an in-process library cache, distinct from the internal Node server cache. + /// + /// public bool UseCache { get; set; } = true; + + /// + /// Indicates whether strict mode is enabled. Default value is "false" + /// public bool StrictMode { get; set; } = false; + + /// + /// Ensures a unique identifier prefix for components on both client and server. + /// It avoids conflicts when using multiple roots on the same page and enables the use of the `useId` hook without conflicts if set to `true`. + /// Default value is "false". + /// IdentifierPrefix requires React in version 18 or higher and is not supported by method. + /// + public bool UseIdentifierPrefix { get; set; } = false; } diff --git a/Forte.Web.React/Forte.Web.React.csproj b/Forte.Web.React/Forte.Web.React.csproj index 8e57b37..ce57eb6 100644 --- a/Forte.Web.React/Forte.Web.React.csproj +++ b/Forte.Web.React/Forte.Web.React.csproj @@ -6,7 +6,8 @@ enable latest true - 1.0.0.0 + 1.0.1.0 + 1.0.1.0 @@ -21,7 +22,7 @@ - + @@ -40,7 +41,7 @@ - + diff --git a/Forte.Web.React/React/ReactService.cs b/Forte.Web.React/React/ReactService.cs index 0506d3c..b9b4276 100644 --- a/Forte.Web.React/React/ReactService.cs +++ b/Forte.Web.React/React/ReactService.cs @@ -56,7 +56,7 @@ public ReactService(INodeJSService nodeJsService, ReactConfiguration config) _config = config; _jsonService = new JsonSerializationService(new ReactJsonSerializerOptions().Options); } - + public ReactService(INodeJSService nodeJsService, IJsonSerializationService jsonService, ReactConfiguration config) { _nodeJsService = nodeJsService; @@ -121,7 +121,8 @@ public async Task RenderToStringAsync(string componentName, object? prop public async Task RenderAsync(TextWriter writer, string componentName, object? props = null, RenderOptions? options = null) { - var component = new Component(componentName, props); + options ??= new RenderOptions(); + var component = new Component(componentName, props, options.ServerOnly ? RenderingMode.Server : RenderingMode.ClientAndServer); Components.Add(component); await writer.WriteAsync($"
").ConfigureAwait(false); @@ -132,8 +133,14 @@ public async Task RenderAsync(TextWriter writer, string componentName, object? p return; } - var result = await InvokeRenderTo(component, props, - options ?? new RenderOptions()).ConfigureAwait(false); + var streamingOptions = new + { + options.EnableStreaming, + options.ServerOnly, + IdentifierPrefix = _config.UseIdentifierPrefix ? component.ContainerId : null, + }; + + var result = await InvokeRenderTo(component, props, streamingOptions).ConfigureAwait(false); using var reader = new StreamReader(await result.Content.ReadAsStreamAsync().ConfigureAwait(false)); @@ -172,7 +179,7 @@ public async Task> GetAvailableComponentNames() return result!; } - + private static Stream GetStreamFromEmbeddedScript(string scriptName) { var currentAssembly = typeof(ReactService).Assembly; @@ -203,7 +210,7 @@ public string GetInitJavascript() private string GetInitJavascriptSource(Component c) { - var shouldHydrate = !_config.IsServerSideDisabled && c.RenderingMode.HasFlag(RenderingMode.Server); + var shouldHydrate = !_config.IsServerSideDisabled && c.RenderingMode == RenderingMode.ClientAndServer; return shouldHydrate ? Hydrate(c) : Render(c); } @@ -220,27 +227,40 @@ private string CreateElement(Component component) private string Render(Component component) { - var bootstrapScript = $"(window.{_config.NameOfObjectToSaveProps} = window.{_config.NameOfObjectToSaveProps} || {{}})[\"{component.JsonContainerId}\"] = {_jsonService.Serialize(component.Props)};"; + var bootstrapScript = + $"(window.{_config.NameOfObjectToSaveProps} = window.{_config.NameOfObjectToSaveProps} || {{}})[\"{component.JsonContainerId}\"] = {_jsonService.Serialize(component.Props)};"; + + var elementById = GetElementById(component.ContainerId); + var element = CreateElement(component); + var options = GetIdentifierPrefix(component); return bootstrapScript + (_config.ReactVersion.Major < 18 - ? $"ReactDOM.render({CreateElement(component)}, {GetElementById(component.ContainerId)}, {{ identifierPrefix: '{component.ContainerId}' }});" - : $"ReactDOMClient.createRoot({GetElementById(component.ContainerId)}).render({CreateElement(component)}, {{ identifierPrefix: '{component.ContainerId}' }});"); + ? $"ReactDOM.render({element}, {elementById});" + : $"ReactDOMClient.createRoot({elementById}{options}).render({element});"); } private string Hydrate(Component component) { + var elementById = GetElementById(component.ContainerId); + var element = CreateElement(component); + var options = GetIdentifierPrefix(component); + return _config.ReactVersion.Major < 18 - ? $"ReactDOM.hydrate({CreateElement(component)}, {GetElementById(component.ContainerId)}, {{ identifierPrefix: '{component.ContainerId}' }});" - : $"ReactDOMClient.hydrateRoot({GetElementById(component.ContainerId)}, {CreateElement(component)}, {{ identifierPrefix: '{component.ContainerId}' }});"; + ? $"ReactDOM.hydrate({element}, {elementById});" + : $"ReactDOMClient.hydrateRoot({elementById}, {element}{options});"; } + + private string GetIdentifierPrefix(Component component) => _config.UseIdentifierPrefix + ? $", {{ identifierPrefix: '{component.ContainerId}' }}" + : string.Empty; } public class RenderOptions { public RenderOptions(bool serverOnly = false, bool enableStreaming = true) { - this.ServerOnly = serverOnly; - this.EnableStreaming = enableStreaming; + ServerOnly = serverOnly; + EnableStreaming = enableStreaming; } public bool ServerOnly { get; } diff --git a/Forte.Web.React/Scripts/renderToPipeableStream.js b/Forte.Web.React/Scripts/renderToPipeableStream.js index da7e8d2..a67f84b 100644 --- a/Forte.Web.React/Scripts/renderToPipeableStream.js +++ b/Forte.Web.React/Scripts/renderToPipeableStream.js @@ -71,7 +71,8 @@ module.exports = ( onError(err) { error = err; console.error(err); - }, + }, + identifierPrefix: options.identifierPrefix, } ); diff --git a/examples/Forte.Web.React.Examples.Core/Pages/Example.cshtml b/examples/Forte.Web.React.Examples.Core/Pages/Example.cshtml index 5a0dee2..21ab5e6 100644 --- a/examples/Forte.Web.React.Examples.Core/Pages/Example.cshtml +++ b/examples/Forte.Web.React.Examples.Core/Pages/Example.cshtml @@ -16,7 +16,7 @@

In Client Side Rendering, the rendering process is deferred until the page is loaded in the browser. This means that the initial HTML page is lightweight and doesn't contain the component's content. Instead, the component is initialized and rendered using JavaScript on the client side. This allows for dynamic updates and interactions.

- @await Html.ReactAsync(new ExampleComponent { Props = Model.Props, RenderingMode = RenderingMode.Client }) + @(await Html.ReactAsync(new ExampleComponent { Props = Model.Props, RenderingMode = RenderingMode.Client }))
@@ -28,7 +28,7 @@

In Server Side Rendering, the component is initially rendered on the server as static markup. This pre-rendered content is sent to the browser, providing faster initial page loads and improved SEO. However, there is no client-side hydration, meaning the component remains static without interactive features.

- @await Html.ReactAsync(new ExampleComponent { Props = Model.Props, RenderingMode = RenderingMode.Server }) + @(await Html.ReactAsync(new ExampleComponent { Props = Model.Props, RenderingMode = RenderingMode.Server }))
@@ -40,7 +40,7 @@

In this approach, the component is first rendered as static markup on the server, similar to SSR. However, additional client-side JavaScript is used to "hydrate" the static markup, enabling interactivity and dynamic behavior. This combines the benefits of both SSR and CSR.

- @await Html.ReactAsync(new ExampleComponent { Props = Model.Props, RenderingMode = RenderingMode.ClientAndServer }) + @(await Html.ReactAsync(new ExampleComponent { Props = Model.Props, RenderingMode = RenderingMode.ClientAndServer }))
diff --git a/examples/Forte.Web.React.Examples.Core/Pages/Example.cshtml.cs b/examples/Forte.Web.React.Examples.Core/Pages/Example.cshtml.cs index a5b93a6..9a7a33c 100644 --- a/examples/Forte.Web.React.Examples.Core/Pages/Example.cshtml.cs +++ b/examples/Forte.Web.React.Examples.Core/Pages/Example.cshtml.cs @@ -12,7 +12,7 @@ public void OnGet(int initCount = 0, string? text = null) Props = new ExampleComponentProps { InitCount = initCount, - Text = text ?? "Use query parameters 'initCount' and 'text' to change values in the React component", + Text = string.IsNullOrEmpty(text) ? "Use query parameters 'initCount' and 'text' to change values in the React component" : text, }; } } \ No newline at end of file diff --git a/examples/Forte.Web.React.Examples.Core/Program.cs b/examples/Forte.Web.React.Examples.Core/Program.cs index dcb67ad..922c937 100644 --- a/examples/Forte.Web.React.Examples.Core/Program.cs +++ b/examples/Forte.Web.React.Examples.Core/Program.cs @@ -32,6 +32,6 @@ var dir = app.Environment.WebRootPath; var js = Directory.GetFiles(Path.Combine(dir, "Client/dist/assets")).First(f => f.EndsWith(".js")); -app.UseReact(new[] { js }, new Version(18, 2, 0), strictMode: true); +app.UseReact(new[] { js }, new Version(18, 2, 0), strictMode: true, useCache: app.Environment.IsDevelopment()); app.Run(); \ No newline at end of file