Advanced Redux — Side Effects, Async Tasks & ThunksEntry 2 of 9

Side Effects in Components with useEffect

The component selects state from the store and runs a side effect in useEffect whenever that state changes.

react.devuseEffect

Notes

  • Do the state change in the reducer first (e.g. add to cart), then sync it to the backend from a component.
  • Selecting the cart with useSelector and listing it as an effect dependency means the effect runs every time the cart changes, keeping the backend up to date.
  • Problem: the effect also runs on the first render, when the cart is still empty. That sends the empty initial cart to the backend and overwrites any saved data.
  • Fixes:
    • changed flag (recommended): reducers that represent user edits set state.changed = true. The effect sends only when cart.changed is true. Loading data from the server doesn't set it.
    • Module-level isInitial flag (course approach): a variable outside the component that skips the first run. Simpler but fragile (React StrictMode runs effects twice in development).

What / Why / When

What

Syncing store state to a backend from a component.

Why

Keeps reducers pure without extra Redux concepts.

When

Small apps, or a single place where the side effect is triggered.

Code compare

React
// ❌ Course starting point: sends the empty cart on first renderfunction App() {  const cart = useAppSelector((state) => state.cart);​  useEffect(() => {    fetch("https://your-project.firebaseio.com/cart.json", {      method: "PUT",      body: JSON.stringify(cart),    });  }, [cart]);​  return <Layout>{/* ... */}</Layout>;}
React
// store/cart-slice.ts (excerpt)type CartState = {  items: CartItem[];  totalQuantity: number;  changed: boolean;};​const cartSlice = createSlice({  name: "cart",  initialState: { items: [], totalQuantity: 0, changed: false } as CartState,  reducers: {    replaceCart(state, action: PayloadAction<{ items: CartItem[]; totalQuantity: number }>) {      state.items = action.payload.items;      state.totalQuantity = action.payload.totalQuantity;      // changed stays false: loading from the server is not a user edit    },    addItem(state, action: PayloadAction<CartItem>) {      state.changed = true;      // ...add or increment the item    },    removeItem(state, action: PayloadAction<string>) {      state.changed = true;      // ...decrement or remove the item    },  },});
React
// ✅ App.tsx: only send after a user edituseEffect(() => {  if (!cart.changed) return;​  fetch("https://your-project.firebaseio.com/cart.json", {    method: "PUT",    body: JSON.stringify({ items: cart.items, totalQuantity: cart.totalQuantity }),  });}, [cart]);

The effect listens to the user actions that change the cart, not to the state itself, so the initial-load problem never happens.

Angular
// cart.service.ts@Injectable({ providedIn: "root" })export class CartService {  private http = inject(HttpClient);  private url = "https://your-project.firebaseio.com/cart.json";​  saveCart(cart: { items: CartItem[]; totalQuantity: number }) {    return this.http.put(this.url, cart);  }​  fetchCart() {    return this.http.get<{ items?: CartItem[]; totalQuantity?: number } | null>(this.url);  }}
Angular
// store/cart.effects.tsimport { inject } from "@angular/core";import { Store } from "@ngrx/store";import { Actions, createEffect, ofType } from "@ngrx/effects";import { concatLatestFrom } from "@ngrx/operators";import { catchError, map, of, switchMap } from "rxjs";​export const saveCart = createEffect(  (actions$ = inject(Actions), store = inject(Store), cartService = inject(CartService)) =>    actions$.pipe(      ofType(CartActions.addItem, CartActions.removeItem),      concatLatestFrom(() => store.select(cartFeature.selectCartState)),      switchMap(([, cart]) =>        cartService.saveCart({ items: cart.items, totalQuantity: cart.totalQuantity }).pipe(          map(() =>            UiActions.showNotification({              status: "success",              title: "Success!",              message: "Cart saved.",            }),          ),          catchError(() =>            of(              UiActions.showNotification({                status: "error",                title: "Error!",                message: "Sending cart data failed!",              }),            ),          ),        ),      ),    ),  { functional: true },);

Differences and gotchas

  • React reacts to state changes ([cart] dependency), so it needs a guard for the first render. NgRx reacts to actions, so loading data never triggers a save.
  • concatLatestFrom (from @ngrx/operators) reads current store state inside an effect, the NgRx counterpart of getState() in a thunk.

Quick reference

TaskReactAngular
TriggeruseEffect(..., [cart])ofType(addItem, removeItem)
Skip initial loadchanged flag / isInitialNot needed
Read state in side effectuseAppSelectorconcatLatestFrom()