From d7e038e91592f49d135a6308f70ff6863c4cab2e Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Wed, 25 Mar 2020 12:26:28 -0500 Subject: [PATCH 1/8] Surface warning/approach for component state --- aspnetcore/blazor/components.md | 3 ++ aspnetcore/blazor/lifecycle.md | 91 ++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/aspnetcore/blazor/components.md b/aspnetcore/blazor/components.md index c930122467c3..2ca4340aefe9 100644 --- a/aspnetcore/blazor/components.md +++ b/aspnetcore/blazor/components.md @@ -134,6 +134,9 @@ In the following example from the sample app, the `ParentComponent` sets the val [!code-razor[](components/samples_snapshot/ParentComponent.razor?highlight=5-6)] +> [!WARNING] +> Don't store component state in a *component parameter*, use a private field instead. For more information, see . + ## Child content Components can set the content of another component. The assigning component provides the content between the tags that specify the receiving component. diff --git a/aspnetcore/blazor/lifecycle.md b/aspnetcore/blazor/lifecycle.md index 97bd91039797..a05ff920088d 100644 --- a/aspnetcore/blazor/lifecycle.md +++ b/aspnetcore/blazor/lifecycle.md @@ -5,7 +5,7 @@ description: Learn how to use Razor component lifecycle methods in ASP.NET Core monikerRange: '>= aspnetcore-3.1' ms.author: riande ms.custom: mvc -ms.date: 03/17/2020 +ms.date: 03/25/2020 no-loc: [Blazor, SignalR] uid: blazor/lifecycle --- @@ -158,6 +158,95 @@ Even if `ShouldRender` is overridden, the component is always initially rendered notifies the component that its state has changed. When applicable, calling `StateHasChanged` causes the component to be rerendered. +Don't store component state in *component parameters*. Parameters are reset when both of the following are true: + +* Child content is rendered with a `RenderFragment`. +* `StateHasChanged` is called in the parent component. + +Consider the following `Expander` component that: + +* Renders child content. +* Toggles showing child content with a component parameter. + +```razor +
+ Toggle (Expanded = @Expanded) + + @if (Expanded) + { + @ChildContent + } +
+ +@code { + [Parameter] + public bool Expanded { get; set; } + + [Parameter] + public RenderFragment ChildContent { get; set; } + + private void Toggle() + { + Expanded = !Expanded; + } +} +``` + +The `Expander` component is added to a parent component that may call `StateHasChanged`: + +```razor + +

Hello, world!

+
+ + + + +``` + +Initially, the `Expander` components behave independently when their `Expanded` properties are toggled. The child components maintain their states as expected. When `StateHasChanged` is called in the parent, the `Expanded` parameter of the first child component is reset back to its initial value (`true`). The second `Expander` component's `Expanded` value isn't reset because no child content is rendered in the second component. + +To maintain state in the preceding scenario, use a *private field* in the `Expander` component to maintain its toggled state. + +The following `Expander` component: + +* Accepts the `Expanded` component parameter value from the parent. +* Assigns the component parameter value to a *private field* (`_expanded`) in the [OnInitialized event](#component-initialization-methods). +* Uses the private field to maintain its internal toggle state. + +```razor +
+ Toggle (Expanded = @_expanded) + + @if (_expanded) + { + @ChildContent + } +
+ +@code { + [Parameter] + public bool Expanded { get; set; } + + [Parameter] + public RenderFragment ChildContent { get; set; } + + private bool _expanded; + + protected override void OnInitialized() + { + _expanded = Expanded; + } + + private void Toggle() + { + _expanded = !_expanded; + } +} +``` + ## Handle incomplete async actions at render Asynchronous actions performed in lifecycle events might not have completed before the component is rendered. Objects might be `null` or incompletely populated with data while the lifecycle method is executing. Provide rendering logic to confirm that objects are initialized. Render placeholder UI elements (for example, a loading message) while objects are `null`. From 3bd165829fb5b5241f588c766132a608d23a5949 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 16 Apr 2020 09:39:51 -0500 Subject: [PATCH 2/8] Move to components topic --- aspnetcore/blazor/components.md | 99 ++++++++++++++++++++++++++++++++- aspnetcore/blazor/lifecycle.md | 93 ------------------------------- 2 files changed, 96 insertions(+), 96 deletions(-) diff --git a/aspnetcore/blazor/components.md b/aspnetcore/blazor/components.md index 2ca4340aefe9..4f16915c3227 100644 --- a/aspnetcore/blazor/components.md +++ b/aspnetcore/blazor/components.md @@ -5,13 +5,13 @@ description: Learn how to create and use Razor components, including how to bind monikerRange: '>= aspnetcore-3.1' ms.author: riande ms.custom: mvc -ms.date: 03/16/2020 +ms.date: 04/16/2020 no-loc: [Blazor, SignalR] uid: blazor/components --- # Create and use ASP.NET Core Razor components -By [Luke Latham](https://github.com/guardrex) and [Daniel Roth](https://github.com/danroth27) +By [Luke Latham](https://github.com/guardrex), [Daniel Roth](https://github.com/danroth27), and [Tobias Bartsch](https://www.aveo-solutions.com/) [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/blazor/common/samples/) ([how to download](xref:index#how-to-download-a-sample)) @@ -393,7 +393,7 @@ Consider the following example: The contents of the `People` collection may change with inserted, deleted, or re-ordered entries. When the component rerenders, the `` component may change to receive different `Details` parameter values. This may cause more complex rerendering than expected. In some cases, rerendering can lead to visible behavior differences, such as lost element focus. -The mapping process can be controlled with the `@key` directive attribute. `@key` causes the diffing algorithm to guarantee preservation of elements or components based on the key's value: +The mapping process can be controlled with the [`@key`](xref:mvc/views/razor#key) directive attribute. `@key` causes the diffing algorithm to guarantee preservation of elements or components based on the key's value: ```csharp @foreach (var person in People) @@ -447,6 +447,99 @@ Generally, it makes sense to supply one of the following kinds of value for `@ke Ensure that values used for `@key` don't clash. If clashing values are detected within the same parent element, Blazor throws an exception because it can't deterministically map old elements or components to new elements or components. Only use distinct values, such as object instances or primary key values. +## Avoid state in component parameters + + notifies the component that its state has changed. When applicable, calling `StateHasChanged` causes the component to be rerendered. + +Don't store component state in *component parameters*. Parameters are reset when both of the following are true: + +* Child content is rendered with a `RenderFragment`. +* `StateHasChanged` is called in the parent component. + +Consider the following `Expander` component that: + +* Renders child content. +* Toggles showing child content with a component parameter. + +```razor +
+ Toggle (Expanded = @Expanded) + + @if (Expanded) + { + @ChildContent + } +
+ +@code { + [Parameter] + public bool Expanded { get; set; } + + [Parameter] + public RenderFragment ChildContent { get; set; } + + private void Toggle() + { + Expanded = !Expanded; + } +} +``` + +The `Expander` component is added to a parent component that may call `StateHasChanged`: + +```razor + +

Hello, world!

+
+ + + + +``` + +Initially, the `Expander` components behave independently when their `Expanded` properties are toggled. The child components maintain their states as expected. When `StateHasChanged` is called in the parent, the `Expanded` parameter of the first child component is reset back to its initial value (`true`). The second `Expander` component's `Expanded` value isn't reset because no child content is rendered in the second component. + +To maintain state in the preceding scenario, use a *private field* in the `Expander` component to maintain its toggled state. + +The following `Expander` component: + +* Accepts the `Expanded` component parameter value from the parent. +* Assigns the component parameter value to a *private field* (`_expanded`) in the [OnInitialized event](#component-initialization-methods). +* Uses the private field to maintain its internal toggle state. + +```razor +
+ Toggle (Expanded = @_expanded) + + @if (_expanded) + { + @ChildContent + } +
+ +@code { + [Parameter] + public bool Expanded { get; set; } + + [Parameter] + public RenderFragment ChildContent { get; set; } + + private bool _expanded; + + protected override void OnInitialized() + { + _expanded = Expanded; + } + + private void Toggle() + { + _expanded = !_expanded; + } +} +``` + ## Partial class support Razor components are generated as partial classes. Razor components are authored using either of the following approaches: diff --git a/aspnetcore/blazor/lifecycle.md b/aspnetcore/blazor/lifecycle.md index a05ff920088d..6b888bb75a8a 100644 --- a/aspnetcore/blazor/lifecycle.md +++ b/aspnetcore/blazor/lifecycle.md @@ -154,99 +154,6 @@ protected override bool ShouldRender() Even if `ShouldRender` is overridden, the component is always initially rendered. -## State changes - - notifies the component that its state has changed. When applicable, calling `StateHasChanged` causes the component to be rerendered. - -Don't store component state in *component parameters*. Parameters are reset when both of the following are true: - -* Child content is rendered with a `RenderFragment`. -* `StateHasChanged` is called in the parent component. - -Consider the following `Expander` component that: - -* Renders child content. -* Toggles showing child content with a component parameter. - -```razor -
- Toggle (Expanded = @Expanded) - - @if (Expanded) - { - @ChildContent - } -
- -@code { - [Parameter] - public bool Expanded { get; set; } - - [Parameter] - public RenderFragment ChildContent { get; set; } - - private void Toggle() - { - Expanded = !Expanded; - } -} -``` - -The `Expander` component is added to a parent component that may call `StateHasChanged`: - -```razor - -

Hello, world!

-
- - - - -``` - -Initially, the `Expander` components behave independently when their `Expanded` properties are toggled. The child components maintain their states as expected. When `StateHasChanged` is called in the parent, the `Expanded` parameter of the first child component is reset back to its initial value (`true`). The second `Expander` component's `Expanded` value isn't reset because no child content is rendered in the second component. - -To maintain state in the preceding scenario, use a *private field* in the `Expander` component to maintain its toggled state. - -The following `Expander` component: - -* Accepts the `Expanded` component parameter value from the parent. -* Assigns the component parameter value to a *private field* (`_expanded`) in the [OnInitialized event](#component-initialization-methods). -* Uses the private field to maintain its internal toggle state. - -```razor -
- Toggle (Expanded = @_expanded) - - @if (_expanded) - { - @ChildContent - } -
- -@code { - [Parameter] - public bool Expanded { get; set; } - - [Parameter] - public RenderFragment ChildContent { get; set; } - - private bool _expanded; - - protected override void OnInitialized() - { - _expanded = Expanded; - } - - private void Toggle() - { - _expanded = !_expanded; - } -} -``` - ## Handle incomplete async actions at render Asynchronous actions performed in lifecycle events might not have completed before the component is rendered. Objects might be `null` or incompletely populated with data while the lifecycle method is executing. Provide rendering logic to confirm that objects are initialized. Render placeholder UI elements (for example, a loading message) while objects are `null`. From efbb9e46a00e7ba18ca7284dabd9a4a9c4ecbce3 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 16 Apr 2020 09:54:15 -0500 Subject: [PATCH 3/8] Update cross-link --- aspnetcore/blazor/components.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/blazor/components.md b/aspnetcore/blazor/components.md index 4f16915c3227..6cd51a191b1f 100644 --- a/aspnetcore/blazor/components.md +++ b/aspnetcore/blazor/components.md @@ -135,7 +135,7 @@ In the following example from the sample app, the `ParentComponent` sets the val [!code-razor[](components/samples_snapshot/ParentComponent.razor?highlight=5-6)] > [!WARNING] -> Don't store component state in a *component parameter*, use a private field instead. For more information, see . +> Don't store component state in a *component parameter*, use a private field instead. For more information, see the [Avoid state in component parameters](#avoid-state-in-component-parameters) section. ## Child content From 10656a5034d190f9cec4155ac4d98d7b80da1d7a Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 16 Apr 2020 09:56:35 -0500 Subject: [PATCH 4/8] Restore State Changes section --- aspnetcore/blazor/lifecycle.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aspnetcore/blazor/lifecycle.md b/aspnetcore/blazor/lifecycle.md index 6b888bb75a8a..f7dd6528509a 100644 --- a/aspnetcore/blazor/lifecycle.md +++ b/aspnetcore/blazor/lifecycle.md @@ -5,7 +5,7 @@ description: Learn how to use Razor component lifecycle methods in ASP.NET Core monikerRange: '>= aspnetcore-3.1' ms.author: riande ms.custom: mvc -ms.date: 03/25/2020 +ms.date: 03/17/2020 no-loc: [Blazor, SignalR] uid: blazor/lifecycle --- @@ -154,6 +154,9 @@ protected override bool ShouldRender() Even if `ShouldRender` is overridden, the component is always initially rendered. +## State changes + notifies the component that its state has changed. When applicable, calling `StateHasChanged` causes the component to be rerendered. + ## Handle incomplete async actions at render Asynchronous actions performed in lifecycle events might not have completed before the component is rendered. Objects might be `null` or incompletely populated with data while the lifecycle method is executing. Provide rendering logic to confirm that objects are initialized. Render placeholder UI elements (for example, a loading message) while objects are `null`. From 0b7966f2c6c44536386fdb462e75858b00c1d80b Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 16 Apr 2020 09:57:35 -0500 Subject: [PATCH 5/8] Nit --- aspnetcore/blazor/lifecycle.md | 1 + 1 file changed, 1 insertion(+) diff --git a/aspnetcore/blazor/lifecycle.md b/aspnetcore/blazor/lifecycle.md index f7dd6528509a..97bd91039797 100644 --- a/aspnetcore/blazor/lifecycle.md +++ b/aspnetcore/blazor/lifecycle.md @@ -155,6 +155,7 @@ protected override bool ShouldRender() Even if `ShouldRender` is overridden, the component is always initially rendered. ## State changes + notifies the component that its state has changed. When applicable, calling `StateHasChanged` causes the component to be rerendered. ## Handle incomplete async actions at render From ae88f018dbce9fe39dd225c7c78069752436c12c Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 16 Apr 2020 10:03:48 -0500 Subject: [PATCH 6/8] Bookmark update --- aspnetcore/blazor/components.md | 2 +- aspnetcore/blazor/lifecycle.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aspnetcore/blazor/components.md b/aspnetcore/blazor/components.md index 2ab31a4bb6de..ca8b4bf90a78 100644 --- a/aspnetcore/blazor/components.md +++ b/aspnetcore/blazor/components.md @@ -508,7 +508,7 @@ To maintain state in the preceding scenario, use a *private field* in the `Expan The following `Expander` component: * Accepts the `Expanded` component parameter value from the parent. -* Assigns the component parameter value to a *private field* (`_expanded`) in the [OnInitialized event](#component-initialization-methods). +* Assigns the component parameter value to a *private field* (`_expanded`) in the [OnInitialized event](xref:blazor/lifecycle#component-initialization-methods). * Uses the private field to maintain its internal toggle state. ```razor diff --git a/aspnetcore/blazor/lifecycle.md b/aspnetcore/blazor/lifecycle.md index 97bd91039797..ec1f340a04a9 100644 --- a/aspnetcore/blazor/lifecycle.md +++ b/aspnetcore/blazor/lifecycle.md @@ -5,7 +5,7 @@ description: Learn how to use Razor component lifecycle methods in ASP.NET Core monikerRange: '>= aspnetcore-3.1' ms.author: riande ms.custom: mvc -ms.date: 03/17/2020 +ms.date: 04/16/2020 no-loc: [Blazor, SignalR] uid: blazor/lifecycle --- @@ -205,7 +205,7 @@ For information on handling errors during lifecycle method execution, see Date: Tue, 21 Apr 2020 14:58:16 -0500 Subject: [PATCH 7/8] React to feedback --- aspnetcore/blazor/components.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/aspnetcore/blazor/components.md b/aspnetcore/blazor/components.md index ca8b4bf90a78..09a18e732901 100644 --- a/aspnetcore/blazor/components.md +++ b/aspnetcore/blazor/components.md @@ -5,7 +5,7 @@ description: Learn how to create and use Razor components, including how to bind monikerRange: '>= aspnetcore-3.1' ms.author: riande ms.custom: mvc -ms.date: 04/16/2020 +ms.date: 04/21/2020 no-loc: [Blazor, SignalR] uid: blazor/components --- @@ -135,7 +135,7 @@ In the following example from the sample app, the `ParentComponent` sets the val [!code-razor[](components/samples_snapshot/ParentComponent.razor?highlight=5-6)] > [!WARNING] -> Don't store component state in a *component parameter*, use a private field instead. For more information, see the [Avoid state in component parameters](#avoid-state-in-component-parameters) section. +> Don't create components that write to their own *component parameters*, use a private field instead. For more information, see the [Don't create components that write to their own parameter properties](#dont-create-components-that-write-to-their-own-parameter-properties) section. ## Child content @@ -449,14 +449,14 @@ Generally, it makes sense to supply one of the following kinds of value for `@ke Ensure that values used for `@key` don't clash. If clashing values are detected within the same parent element, Blazor throws an exception because it can't deterministically map old elements or components to new elements or components. Only use distinct values, such as object instances or primary key values. -## Avoid state in component parameters +## Don't create components that write to their own parameter properties - notifies the component that its state has changed. When applicable, calling `StateHasChanged` causes the component to be rerendered. +Parameters are overwritten under the following conditions: -Don't store component state in *component parameters*. Parameters are reset when both of the following are true: +* A child content is rendered with a `RenderFragment`. +* is called in the parent component. -* Child content is rendered with a `RenderFragment`. -* `StateHasChanged` is called in the parent component. +Parameters are reset because the parent component rerenders when is called and new parameter values are supplied to the child component. Consider the following `Expander` component that: From 084b9d94d8639691a197203c06d265b93563dda8 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Tue, 21 Apr 2020 15:01:09 -0500 Subject: [PATCH 8/8] Nit --- aspnetcore/blazor/components.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/blazor/components.md b/aspnetcore/blazor/components.md index 09a18e732901..3d90b2b4c743 100644 --- a/aspnetcore/blazor/components.md +++ b/aspnetcore/blazor/components.md @@ -453,7 +453,7 @@ Ensure that values used for `@key` don't clash. If clashing values are detected Parameters are overwritten under the following conditions: -* A child content is rendered with a `RenderFragment`. +* A child component's content is rendered with a `RenderFragment`. * is called in the parent component. Parameters are reset because the parent component rerenders when is called and new parameter values are supplied to the child component.