Skip to content

Commit

Permalink
Fixed identifier prefix support and added an optional setting to swit…
Browse files Browse the repository at this point in the history
…ch it on

# Added
- #34
- Documentation annotations for `ReactConfiguration`

# Fixed
- #32
- #33
- Code references in Demo project

* Added switch for identifier prefix and added support in renderToPipeableStream.

* Fixed .net core example

* Updated version and nuget packages

* Fixed hydration condition based on options

* Optional prefix identifier in RenderAsync

* Moved upgraded nuget to proper section
  • Loading branch information
AyronK authored Nov 24, 2023
1 parent ee0498a commit 1b0020b
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 22 deletions.
35 changes: 35 additions & 0 deletions Forte.Web.React/Configuration/ReactConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,49 @@
using System;
using System.Collections.Generic;
using Forte.Web.React.React;

namespace Forte.Web.React.Configuration;

public class ReactConfiguration
{
/// <summary>
/// Collection of URLs pointing to scripts.
/// </summary>
public List<string> ScriptUrls { get; set; } = new();

/// <summary>
/// Indicates whether server-side rendering is globally disabled. Default value is "false".
/// </summary>
public bool IsServerSideDisabled { get; set; } = false;

/// <summary>
/// Version of React being used.
/// </summary>
public Version ReactVersion { get; set; } = null!;

/// <summary>
/// Name of the object used to save properties. Default value is "__reactProps".
/// </summary>
public string NameOfObjectToSaveProps { get; set; } = "__reactProps";

/// <summary>
/// Indicates whether caching is used. Default value is "true".
/// <remarks>
/// This property specifically controls the usage of an in-process library cache, distinct from the internal Node server cache.
/// </remarks>
/// </summary>
public bool UseCache { get; set; } = true;

/// <summary>
/// Indicates whether strict mode is enabled. Default value is "false"
/// </summary>
public bool StrictMode { get; set; } = false;

/// <summary>
/// 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".
/// <remarks>IdentifierPrefix requires React in version 18 or higher and is not supported by <see cref="IReactService.RenderToStringAsync"/> method.</remarks>
/// </summary>
public bool UseIdentifierPrefix { get; set; } = false;
}
7 changes: 4 additions & 3 deletions Forte.Web.React/Forte.Web.React.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<Packable>true</Packable>
<VersionPrefix>1.0.0.0</VersionPrefix>
<VersionPrefix>1.0.1.0</VersionPrefix>
<Version>1.0.1.0</Version>
</PropertyGroup>

<PropertyGroup>
Expand All @@ -21,7 +22,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Jering.Javascript.NodeJS" Version="7.0.0-beta.4"/>
<PackageReference Include="Jering.Javascript.NodeJS" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
Expand All @@ -40,7 +41,7 @@

<ItemGroup Condition=" '$(TargetFramework)' == 'net48' ">
<Reference Include="System.Web"/>
<PackageReference Include="Microsoft.AspNet.Mvc" Version="5.2.9"/>
<PackageReference Include="Microsoft.AspNet.Mvc" Version="5.3.0" />
</ItemGroup>

</Project>
46 changes: 33 additions & 13 deletions Forte.Web.React/React/ReactService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -121,7 +121,8 @@ public async Task<string> 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($"<div id=\"{component.ContainerId}\">").ConfigureAwait(false);
Expand All @@ -132,8 +133,14 @@ public async Task RenderAsync(TextWriter writer, string componentName, object? p
return;
}

var result = await InvokeRenderTo<HttpResponseMessage>(component, props,
options ?? new RenderOptions()).ConfigureAwait(false);
var streamingOptions = new
{
options.EnableStreaming,
options.ServerOnly,
IdentifierPrefix = _config.UseIdentifierPrefix ? component.ContainerId : null,
};

var result = await InvokeRenderTo<HttpResponseMessage>(component, props, streamingOptions).ConfigureAwait(false);

using var reader = new StreamReader(await result.Content.ReadAsStreamAsync().ConfigureAwait(false));

Expand Down Expand Up @@ -172,7 +179,7 @@ public async Task<IReadOnlyCollection<string>> GetAvailableComponentNames()

return result!;
}

private static Stream GetStreamFromEmbeddedScript(string scriptName)
{
var currentAssembly = typeof(ReactService).Assembly;
Expand Down Expand Up @@ -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);
}

Expand All @@ -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; }
Expand Down
3 changes: 2 additions & 1 deletion Forte.Web.React/Scripts/renderToPipeableStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ module.exports = (
onError(err) {
error = err;
console.error(err);
},
},
identifierPrefix: options.identifierPrefix,
}

);
Expand Down
6 changes: 3 additions & 3 deletions examples/Forte.Web.React.Examples.Core/Pages/Example.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<p>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.</p>
</div>
<div class="example--container">
@await Html.ReactAsync(new ExampleComponent { Props = Model.Props, RenderingMode = RenderingMode.Client })
@(await Html.ReactAsync<ExampleComponent, ExampleComponentProps>(new ExampleComponent { Props = Model.Props, RenderingMode = RenderingMode.Client }))
</div>
</div>

Expand All @@ -28,7 +28,7 @@
<p>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.</p>
</div>
<div class="example--container">
@await Html.ReactAsync(new ExampleComponent { Props = Model.Props, RenderingMode = RenderingMode.Server })
@(await Html.ReactAsync<ExampleComponent, ExampleComponentProps>(new ExampleComponent { Props = Model.Props, RenderingMode = RenderingMode.Server }))
</div>
</div>

Expand All @@ -40,7 +40,7 @@
<p>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.</p>
</div>
<div class="example--container">
@await Html.ReactAsync(new ExampleComponent { Props = Model.Props, RenderingMode = RenderingMode.ClientAndServer })
@(await Html.ReactAsync<ExampleComponent, ExampleComponentProps>(new ExampleComponent { Props = Model.Props, RenderingMode = RenderingMode.ClientAndServer }))
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
}
2 changes: 1 addition & 1 deletion examples/Forte.Web.React.Examples.Core/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

0 comments on commit 1b0020b

Please sign in to comment.