Skip to content

emit_new_bloc_state_instances

v1.0.0WarningConfigurableBloc / Riverpod

This rule flags an emit(state) call that hands the current state object straight back to emit. Bloc compares the incoming state with == and drops it when it is equal to the current one, so the call does nothing.

BlocBase.emit short-circuits when the new state equals the current one. Passing state straight back is therefore always a no-op: no listener is notified, no widget rebuilds, and the UI keeps rendering the previous data.

Nothing throws and the handler runs to completion, so the symptom — a list that will not refresh — looks like a rendering problem and gets debugged in the widget.

See also: bloc: why doesn’t my state update?, bloc: BlocBase.emit

The commonest form. The list really does gain an item; no one is told:

// Don't
class TodoCubit extends Cubit<TodoState> {
TodoCubit() : super(TodoState.initial());
void add(Todo todo) {
state.items.add(todo);
emit(state); // LINT - same instance, `==` holds, dropped
}
}
// Do - a new instance, so the equality check sees a change
class TodoCubit extends Cubit<TodoState> {
TodoCubit() : super(TodoState.initial());
void add(Todo todo) {
emit(state.copyWith(items: [...state.items, todo]));
}
}

Anything derived from the state is accepted - state.copyWith(...), state + 1, or a fresh construction:

void increment() => emit(state + 1);
void reset() => emit(TodoState.initial());

This is also why Bloc state is expected to be immutable, and what prefer_immutable_bloc_state asks for from the other end: that rule makes the state impossible to mutate in place, this one catches the mutation-and-re-emit directly.

In a Bloc, emit is the handler’s Emitter parameter rather than an inherited method. Both call shapes are reported:

// Don't
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Refresh>((event, emit) => emit(state)); // LINT
}
}

If the intent was “handle this event by doing nothing”, write nothing - an empty handler body says so, a no-op emit does not:

// Do
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Refresh>((event, emit) {});
}
}

If your blocs emit through a helper, name it so the same no-op is caught there:

many_lints.yaml
rules:
emit_new_bloc_state_instances:
additional_methods: [safeEmit]
// Don't
class TodoCubit extends Cubit<TodoState> {
TodoCubit() : super(TodoState.initial());
void refresh() => safeEmit(state); // LINT
void safeEmit(TodoState next) {
if (!isClosed) emit(next);
}
}

Only the bare reads state and this.state are reported. Anything built from the state is left alone, including state.copyWith() with no arguments - that returns a new instance, so emit accepts it even though the contents are identical.

A state object reached through a local variable (final s = state; emit(s);) is not reported. Tracking assignments through the method body would be needed for a shape that is rare in practice.

Only a single-argument call is checked. A wrapper invoked as safeEmit(state, force: true) is not matched.

This rule is in the core preset, so it is on with preset: core, preset: recommended or preset: opinionated.

To turn it off:

many_lints.yaml
rules:
emit_new_bloc_state_instances: false

To keep the rule on but skip certain paths, use per-rule exclude.

analysis_options.yaml
many_lints:
rules:
emit_new_bloc_state_instances:
additional_methods: [safeEmit]
Option Type Default Description
additional_methods list of strings [] Extra methods treated like emit, for a project wrapper that forwards to it