Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed identifier prefix support and added an optional setting to switch it on #31

Merged
merged 6 commits into from
Nov 24, 2023
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
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 @@
_config = config;
_jsonService = new JsonSerializationService(new ReactJsonSerializerOptions().Options);
}

public ReactService(INodeJSService nodeJsService, IJsonSerializationService jsonService, ReactConfiguration config)
{
_nodeJsService = nodeJsService;
Expand All @@ -71,7 +71,7 @@
{
component.Path,
component.JsonContainerId,
props,

Check warning on line 74 in Forte.Web.React/React/ReactService.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'item' in 'void List<object>.Add(object item)'.

Check warning on line 74 in Forte.Web.React/React/ReactService.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'item' in 'void List<object>.Add(object item)'.

Check warning on line 74 in Forte.Web.React/React/ReactService.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'item' in 'void List<object>.Add(object item)'.

Check warning on line 74 in Forte.Web.React/React/ReactService.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'item' in 'void List<object>.Add(object item)'.
_config.ScriptUrls,
_config.NameOfObjectToSaveProps,
};
Expand Down Expand Up @@ -121,7 +121,8 @@
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 @@
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 @@

return result!;
}

private static Stream GetStreamFromEmbeddedScript(string scriptName)
{
var currentAssembly = typeof(ReactService).Assembly;
Expand Down Expand Up @@ -203,7 +210,7 @@

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 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();