Ciclo de Vida e Parâmetros de Componentes no Blazor

Published on 2026-10-15·v1.0

Objective

Um componente Razor é uma classe C# com ciclo de vida: o framework cria a instância, entrega os parâmetros, renderiza e depois descarta. Os hooks desse ciclo são onde moram a maioria dos bugs de Blazor: dado carregado duas vezes, callback que muda o estado mas não a tela, StateHasChanged que lança exceção, lista cujos inputs mantêm o texto errado depois de uma ordenação. Este concept registra em log todos os hooks em um app real (SDK 10.0.100, Interactive Server, prerender ligado) e mostra o que roda, em que ordem, na instância do prerender e na interativa, além dos mecanismos de parâmetro que alimentam o ciclo: [Parameter], EventCallback, valores em cascata e @key. Render modes e prerender estão em Blazor Render Modes and Hosting Models, e o roteamento em Blazor Routing, Layouts and Navigation.

Use Cases

  • Carregar dados no hook certo, sem uma segunda consulta vinda do prerender e sem um flash de conteúdo vazio.
  • Reagir a uma mudança de parâmetro (um id de rota, um filtro) recarregando, e não recarregar quando um re-render do pai não tem relação com isso.
  • Ligar um filho ao pai com EventCallback para que a tela atualize.
  • Limpar assinaturas e timers quando o usuário sai da página.

Deep Dive

Os hooks, medidos

Um componente filho sobrescreve todos os hooks e registra cada um, com o renderer (RendererInfo.IsInteractive) entre parênteses. O pai passa um int Value, uma List<int> e uma string. A primeira requisição, o prerender, registrou:

text
SetParametersAsync start (prerender) OnInitialized OnInitializedAsync start / end OnParametersSet value=0 OnParametersSetAsync ShouldRender SetParametersAsync end Dispose

SetParametersAsync envolve tudo: a implementação base atribui as propriedades [Parameter], depois chama os hooks de inicialização (só na primeira vez) e OnParametersSet{Async} (toda vez). OnAfterRender não aparece, e a instância foi descartada logo após o render: um componente pré-renderizado é jogado fora quando a resposta termina, e o navegador depois cria uma instância nova. Na instância interativa a mesma sequência reaparece, agora com OnAfterRender first=True, e as mudanças de parâmetro seguintes registram só SetParametersAsync, OnParametersSet{Async}, ShouldRender e OnAfterRender first=False.

O detalhe que surpreende está no meio do log interativo:

text
OnInitializedAsync start OnAfterRender first=True <- o primeiro render aconteceu aqui OnInitializedAsync end (depois de 300 ms de delay) OnParametersSet value=0

Quando OnInitializedAsync realmente espera algo, o framework renderiza o componente no primeiro await e renderiza de novo quando a task termina. O componente precisa então tratar o estado "ainda não carregou" (items is null), e OnAfterRender(firstRender: true) pode rodar antes de o dado chegar. Se o método termina de forma síncrona, há um único render.

Para que serve o OnAfterRender

OnAfterRender{Async} roda quando o DOM existe, então é o lugar do JavaScript interop. No prerender não há navegador para chamar. Chamar IJSRuntime em OnInitializedAsync registrou:

text
InvalidOperationException: JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method.

A mesma chamada dentro de if (firstRender) em OnAfterRenderAsync funcionou. Não chame StateHasChanged incondicionalmente em OnAfterRender: ele agenda outro render, que roda OnAfterRender de novo, um loop infinito. Proteja com firstRender ou com uma checagem de estado.

Quando os parâmetros são atribuídos de novo

Clicar em um botão do pai que só muda um contador other sem relação fez o filho com List<int> rodar SetParametersAsync e OnParametersSet outra vez, com os mesmos valores. Um segundo filho só com int e string não rodou:

text
[prim] OnParametersSet value=0 (inicial) (clique em "other": nada registrado) [prim] OnParametersSet value=1 (clique em "v": um parâmetro mudou)

A documentação explica: os hooks rodam quando o pai re-renderiza e fornece tipos primitivos ou imutáveis conhecidos com pelo menos uma mudança, ou quando existe qualquer parâmetro de tipo complexo, porque o framework não tem como saber se um objeto mutável mudou por dentro. Então OnParametersSetAsync não significa "o parâmetro mudou". Compare com o valor anterior você mesmo antes de recarregar:

csharp
[Parameter] public int ProductId { get; set; } private int loadedId; protected override async Task OnParametersSetAsync() { if (ProductId == loadedId) return; // re-render do pai com o mesmo id: ignora loadedId = ProductId; product = await Service.GetAsync(ProductId); }

Sobrescrever ShouldRender para devolver false pula o render, mas não pula os hooks de parâmetro. OnInitializedAsync é o lugar errado para carregar por parâmetro, porque roda uma vez por instância e uma navegação para o mesmo componente com outro id reaproveita a instância.

EventCallback versus um delegate

Um filho que avisa o pai recebe um EventCallback ou EventCallback<T>. O mesmo filho tinha dois botões, um chamando EventCallback<int> e outro Action<int>, ambos ligados a um método do pai que fazia picked += n:

text
clique callback -> tela: picked=1 clique action -> tela: picked=1 (o campo valia 2, a UI não foi re-renderizada) clique "other" -> tela: picked=2 (qualquer render posterior mostra)

EventCallback.InvokeAsync chama o handler e depois re-renderiza o componente dono dele, que é o que a documentação quer dizer com StateHasChanged chamado automaticamente. Um Action ou Func<Task> puro executa o código e nada mais. O EventCallback também tolera parâmetro não informado (chamar não faz nada), e o EventCallback<T> entra no @bind pela convenção ValueChanged.

StateHasChanged e o dispatcher

Um componente atualiza a tela enfileirando um render. Handlers que o próprio framework invoca (eventos, EventCallback, hooks de ciclo de vida) já rodam no contexto de sincronização do renderer. Código que acorda em outro lugar (um Task.Run, um timer, um evento de um service) não. Chamar StateHasChanged ali lançou:

text
InvalidOperationException: The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.

A correção é await InvokeAsync(StateHasChanged);, e a mudança de estado feita por esse código externo também deve ficar dentro do InvokeAsync. No teste, o campo já tinha sido alterado pela thread errada antes da exceção, uma data race que a exceção só denuncia pela metade.

Valores em cascata

<CascadingValue Value="theme"> disponibiliza um valor para todo descendente com um [CascadingParameter] do tipo correspondente (ou do Name). Quando o valor do pai muda, os descendentes assinantes rodam OnParametersSet de novo:

plaintext
<CascadingValue Value="theme"><Themed /></CascadingValue> <CascadingValue Value="theme" IsFixed="true"><Themed /></CascadingValue>

Depois de trocar theme de dark para light, só o primeiro Themed registrou OnParametersSet theme=light. IsFixed dispensa a assinatura, o que a documentação apresenta como a opção mais barata quando o valor nunca muda. Dois limites da mesma página: parâmetros em cascata não atravessam a fronteira de render mode (uma página estática não consegue cascatear um valor para um componente interativo), e para estado do app inteiro o caminho suportado é AddCascadingValue como service de nível raiz, porque ele fica disponível para componentes interativos.

@key e descarte

O Blazor compara uma lista por posição. Duas listas de três componentes Row, cada um com um <input> preenchido com typed-0, typed-1, typed-2, e depois invertidas:

text
sem @key: c (instance 13)|typed-0 b (instance 14)|typed-1 a (instance 15)|typed-2 com @key: c (instance 18)|typed-2 b (instance 17)|typed-1 a (instance 16)|typed-0

Sem @key as instâncias ficaram no lugar e só o parâmetro Label mudou, então o texto no input do DOM deixou de corresponder ao item. Com @key="n" as instâncias acompanharam seus itens, e o texto digitado também. Use uma identidade estável (um id, não o índice do loop) onde as linhas guardam estado.

IDisposable (ou IAsyncDisposable) é o hook para cancelar assinaturas. Além da instância do prerender, o Dispose rodou também para a instância interativa depois que a aba navegou para outra página, alguns segundos depois, quando o circuito foi encerrado. Um timer ou assinatura de evento criado em OnInitialized e nunca liberado mantém o componente vivo no servidor.

Trade-offs

  • Carregar em OnInitializedAsync roda duas vezes com prerender. O prerender e a instância interativa executam o método, então a consulta acontece duas vezes, a menos que o estado seja persistido entre as duas.
    csharp
    [PersistentState] public List<Product>? Products { get; set; } protected override async Task OnInitializedAsync() => Products ??= await Service.ListAsync();
  • OnParametersSet é um sinal de "o pai renderizou", não de mudança. Um parâmetro mutável o faz disparar em todo render do pai, então recarregue só quando houver mudança real.
  • EventCallback é o padrão seguro de filho para pai. Um delegate puro serve para um helper que não mexe em estado, mas não atualiza o pai.
  • @key custa memória e trabalho de diff. Vale para linhas com estado ou animação, e é ruído em uma lista estática.
  • Valor em cascata fixo é rápido e congelado. Um valor que será trocado depois precisa de um CascadingValue não fixo, e todo assinante paga por isso.
  • StateHasChanged em handler é um cheiro. Eventos já re-renderizam; chamar na mão geralmente indica código fora do dispatcher que precisa de InvokeAsync.

Documentation Links