Learning path · Level 10 · Tutorial 6

Share Scoped Client State with Context

Share a focused piece of client state across routed views using context and a reducer. Place the provider deliberately so state survives the intended navigation without becoming application-wide by default.

  • Level: Intermediate
  • Duration: 18 min
  • 8 steps
Start tutorial Back to the path
Share Scoped Client State with Context

What you will go through

  1. Separate State Ownership from Context Distinguish reducer state ownership from context delivery, then identify the Books feature as the focused subtree for the shared shortlist. 2 min
  2. Define a Typed Context Contract Declare a typed context contract for Books shortlist state and dispatch, using null to represent a missing provider. 2 min
  3. Provide Reducer-Owned State Implement a focused provider that owns reducer state and supplies one shared context value to its descendants. 2 min
  4. Read Context Through a Checked Hook Create a checked consumer Hook that returns the shared shortlist contract to valid descendants and clearly reports a missing provider. 2 min
  5. Share One Provider Across Routed Views Place one feature-scoped provider in a shared route layout so the Books list, shortlist view, and summary use the same reducer-owned client state. 3 min
  6. Observe the Provider's State Lifetime Predict and test when the Books shortlist remains in memory and when a provider remount initializes it again. 3 min
  7. Keep Unrelated State Out Assign shared selection, temporary interaction details, addressable filters, and derived values to focused owners. 2 min
  8. Apply and Verify the Scoped Design Repair and verify a feature-scoped context design that shares a Books shortlist across related routes while preserving clear ownership and lifetime boundaries. 2 min

What you will learn

  • Create a typed context provider that exposes reducer state and dispatch.
  • Write a consumer Hook that reports a missing provider clearly.
  • Place the provider so relevant views share state across navigation.
  • Demonstrate how remounting the provider changes the lifetime of its state.
  • Keep local interaction details and URL-controlled filters out of unrelated context.

Before you start

  • Model Client-State Transitions with Reducers
  • Extract Reusable Stateful Logic into Custom Hooks
  • Lift Shared State to a Common Parent
  • Build Accessible Client-Side Routes with React Router

Step 1 of 8

Separate State Ownership from Context

Distinguish reducer state ownership from context delivery, then identify the Books feature as the focused subtree for the shared shortlist.

Separate the two responsibilities

State ownership and delivery

Context delivers; an owner retains

Context distributes a value through a component subtree. It is a delivery mechanism, not state storage by itself.

For the Books feature, a component can own the shortlist reducer state. A provider then makes that component’s current value available below it. These are separate responsibilities:

  • State owner: retains the reducer state and receives dispatched actions.
  • Provider: supplies the owner’s current value to descendants.
  • Consumer: reads the nearest matching provider above it in the component tree.

A consumer does not receive state from a sibling or from a provider elsewhere in the tree. It follows its ancestor path upward until it finds the nearest matching provider.

One value travels through the Books subtree

A component-tree diagram showing reducer-owned state flowing through a provider to two Books descendants, while an unrelated About branch remains outside the feature boundary.

The provider distributes the owner’s current value only to descendants in its subtree. A consumer uses the nearest matching provider above it.

Find the relevant subtree

Feature scope, not automatic global scope

Choose the smallest useful scope

The Books list and its shortlist view are related: both need the same shared client state while someone moves between those feature views. That makes the Books subtree the useful scope.

About has no need to read or change the shortlist. Putting the value above every route would make it available there without a demonstrated requirement.

Start with the smallest subtree containing every consumer that genuinely needs the value. You can widen that boundary later only when a new requirement proves it necessary.

Tip

Keep the decision deliberate

Context can avoid passing a shared value through many intermediate components. It does not change the ownership decision: first identify who should retain the state, then distribute that owner’s value to the consumers that need it.

Check the ownership decision

Choose the ownership boundary

A Books list and a shortlist view must share one selection, but About does not use it. Which design identifies both the owner and the appropriate scope?

Step 2 of 8

Define a Typed Context Contract

Declare a typed context contract for Books shortlist state and dispatch, using null to represent a missing provider.

Prepare the feature contracts

Local baseline

Start from a self-contained local baseline

This step does not require files from an earlier tutorial. Create a Vite React–TypeScript project, install React Router, and create the Books feature folder.

The reducer below is supplied reference code. In this step, you will define a separate context contract that exposes its state and typed dispatch.

Create the project and install the router

Run these commands in your terminal.

bash
npm create vite@latest books-context -- --template react-ts
cd books-context
npm install
npm install react-router
mkdir -p src/features/books
Expected output
◇  Scaffolding project in ./books-context...
◇  Done. Now run:

  cd books-context
  npm install
  npm run dev

added packages
found 0 vulnerabilities

added react-router
found 0 vulnerabilities

This is representative output. Exact package versions, package counts, and audit messages can vary. The important signals are that Vite scaffolds the project and both installations finish successfully.

Local book fixtures

Create src/features/books/books.ts. These are local fixture records, not fetched resources.

typescript
export type Book = {
  id: string;
  title: string;
};

export const books: Book[] = [
  { id: "b1", title: "Practical CSS" },
  { id: "b2", title: "TypeScript Patterns" },
  { id: "b3", title: "React Interfaces" },
];

Supplied shortlist reducer

Create src/features/books/shortlist.ts. This supplied reducer remains responsible for transition rules; the context contract will only describe the value shared with descendants.

typescript
export type ShortlistState = {
  selectedIds: string[];
};

export type ShortlistAction =
  | { type: "bookToggled"; bookId: string }
  | { type: "selectionCleared" };

export const initialShortlistState: ShortlistState = {
  selectedIds: [],
};

export function shortlistReducer(
  state: ShortlistState,
  action: ShortlistAction,
): ShortlistState {
  switch (action.type) {
    case "bookToggled": {
      const selectedIds = state.selectedIds.includes(action.bookId)
        ? state.selectedIds.filter((id) => id !== action.bookId)
        : [...state.selectedIds, action.bookId];

      return { selectedIds };
    }
    case "selectionCleared":
      return initialShortlistState;
  }
}

Declare one honest contract

State type versus context value type

Describe the distributed value

ShortlistState describes reducer data only: the selected book IDs. A context value needs a wider contract because descendants need both the current data and a typed way to request a transition.

ShortlistContextValue therefore contains state and dispatch. It is distinct from ShortlistState rather than a replacement for it.

Declare the context once at module level. That exported object is the shared context identity that a future provider and future consumers will both use.

One shared context identity

Diagram showing selected book markers and an action symbol combined in one module-level context object, then distributed to two descendant components.

The context value packages reducer state and typed dispatch; one module-level context object gives future participants the same identity.

Typed nullable context contract

Create src/features/books/ShortlistContext.ts. This module declares a contract only: it does not create reducer state and does not render a provider.

typescript
import { createContext } from "react";
import type { Dispatch } from "react";
import type {
  ShortlistAction,
  ShortlistState,
} from "./shortlist";

export type ShortlistContextValue = {
  state: ShortlistState;
  dispatch: Dispatch<ShortlistAction>;
};

export const ShortlistContext =
  createContext<ShortlistContextValue | null>(null);

Warning

Why the default is null

null is intentional: it means no matching provider has supplied a value. Do not use an empty state and a no-op dispatch as the default. Those values would appear usable and could hide an incorrect component-tree configuration.

The real initial state remains initialShortlistState in the reducer module. The explicit null sentinel preserves the distinction between an absent provider and a supplied shortlist value.

Practice: complete the contract

Build the context declaration

Complete the contract in ShortlistContext.ts. Keep the missing-provider case explicit rather than fabricating state.

typescript
import { createContext } from "react";
import type { Dispatch } from "react";
import type {
  ShortlistAction,
  ShortlistState,
} from "./shortlist";

export type  = {
  state: ShortlistState;
  dispatch: ;
};

export const ShortlistContext =
  createContext<>();

Step 3 of 8

Provide Reducer-Owned State

Implement a focused provider that owns reducer state and supplies one shared context value to its descendants.

Connect ownership, delivery, and descendants

One owner, delivered value

Context distributes a value through a subtree; it does not own the shortlist by itself. In this feature, ShortlistProvider is the state owner: its reducer holds the current selectedIds, and its context value delivers both state and dispatch to descendants.

A descendant interaction dispatches an action to that one reducer. React calculates the next state, then the provider supplies the updated value to every consumer below it.

State flow inside one provider instance

A diagram showing a descendant action flowing through dispatch and a reducer in ShortlistProvider, then updated state flowing to a book list and shortlist summary.

The provider owns the reducer state. Context is the delivery path from that owner to descendant consumers.

Tip

Provider boundaries matter

The provider must wrap a consumer in the rendered component tree. A component is not below a provider merely because it returns provider JSX: its own code runs before that returned provider exists. Put the consumer in a descendant component instead.

Build ShortlistProvider

Implement the state owner

Create src/features/books/ShortlistProvider.tsx. The reducer and context contract already exist; this component connects them. useReducer creates state for this mounted provider instance, while the explicit provider element makes the current contract available below it.

src/features/books/ShortlistProvider.tsx

ShortlistProvider owns reducer state and supplies the typed context value to its children.

tsx
import { useReducer } from "react";
import type { ReactNode } from "react";
import { ShortlistContext } from "./ShortlistContext";
import type { ShortlistContextValue } from "./ShortlistContext";
import {
  initialShortlistState,
  shortlistReducer,
} from "./shortlist";

type ShortlistProviderProps = {
  children: ReactNode;
};

export function ShortlistProvider({
  children,
}: ShortlistProviderProps) {
  const [state, dispatch] = useReducer(
    shortlistReducer,
    initialShortlistState,
  );

  const value: ShortlistContextValue = { state, dispatch };

  return (
    <ShortlistContext.Provider value={value}>
      {children}
    </ShortlistContext.Provider>
  );
}

Recognize independent provider instances

One instance versus two

Each mounted ShortlistProvider calls useReducer for itself. Therefore, two separately mounted provider instances have two independent selectedIds arrays and two dispatch functions.

A dispatch from a descendant of Provider A updates only Provider A. To make multiple descendants share one shortlist, place all of them beneath the same provider instance.

Provider instances determine sharing

A comparison between one ShortlistProvider shared by two panels and two separate providers each owning different shortlist state.

One enclosing provider creates shared client state for its descendants. Separate providers create independent state owners.

Practice the provider connection

Wire the provider and identify its scope

Complete the provider fragment and the two instance relationships.

tsx
export function ShortlistProvider({
  children,
}: ShortlistProviderProps) {
  const [state, dispatch] = ;

  const value: ShortlistContextValue = ;

  return (
    <ShortlistContext.Provider value={value}>
      {children}
    </ShortlistContext.Provider>
  );
}

// With one provider wrapping both views, they use  provider instance.
// With one provider around each view, they use  provider instances.

Step 4 of 8

Read Context Through a Checked Hook

Create a checked consumer Hook that returns the shared shortlist contract to valid descendants and clearly reports a missing provider.

Read the supplied value

Read the supplied contract

ShortlistProvider already owns the reducer state. A consumer should read the value that this provider supplies rather than create another reducer.

Create one consumer Hook, useShortlist(). It calls useContext(ShortlistContext) and returns the shared contract: state plus dispatch. Calling this Hook in several descendants reads the same provider-owned value; the Hook does not create state of its own.

One provider, many readers

Diagram showing two descendants reading the same state and dispatch from ShortlistProvider, while a consumer without a provider receives null.

The Hook reads a provider value. It does not create another reducer instance.

Tip

Make missing placement explicit

The context was deliberately created with null as its default. That value is not usable application state—it is evidence that no matching provider exists above the consumer.

Check for null once inside the Hook. TypeScript then narrows the remaining return value to ShortlistContextValue, so components need neither ! nor repeated provider checks.

Use state and dispatch safely

useShortlist.ts

In src/features/books/useShortlist.ts, put the provider check at the boundary used by every consumer.

typescript
import { useContext } from "react";
import { ShortlistContext } from "./ShortlistContext";
import type { ShortlistContextValue } from "./ShortlistContext";

export function useShortlist(): ShortlistContextValue {
  const value = useContext(ShortlistContext);

  if (value === null) {
    throw new Error("useShortlist must be used within ShortlistProvider.");
  }

  return value;
}

Consumers use the same contract

Consumers destructure the checked contract. A summary reads derived display data; a control dispatches a semantic action to the existing reducer owner.

tsx
import { useShortlist } from "./useShortlist";

export function ShortlistSummary() {
  const { state } = useShortlist();

  return <p>Shortlist: {state.selectedIds.length}</p>;
}

function ShortlistToggle({ bookId }: { bookId: string }) {
  const { state, dispatch } = useShortlist();
  const isSelected = state.selectedIds.includes(bookId);

  return (
    <button
      type="button"
      onClick={() => dispatch({ type: "bookToggled", bookId })}
    >
      {isSelected ? "Remove from shortlist" : "Add to shortlist"}
    </button>
  );
}

Why throw here?

The error identifies a configuration problem at the point where it occurs: a component called the feature Hook without being below ShortlistProvider. Fix the component tree placement; do not invent fallback state or a no-op dispatch.

Check the contract and scope

Finish the checked Hook

Complete the missing-provider guard and the checked return path.

typescript
export function useShortlist(): ShortlistContextValue {
  const value = useContext(ShortlistContext);

  if (value === ) {
    throw new Error();
  }

  return ;
}

Inspect provider scope

Which diagram has a consumer calling useShortlist outside its required provider?

Step 5 of 8

Share One Provider Across Routed Views

Place one feature-scoped provider in a shared route layout so the Books list, shortlist view, and summary use the same reducer-owned client state.

Choose the shared subtree

Place the provider at the shared route boundary

A context provider shares its value only with descendants in its component subtree. Put one ShortlistProvider in BooksLayout, the shared layout for both Books routes.

ShortlistSummary and Outlet are siblings beneath that provider. When the child route changes from the Books list to the Shortlist view, the layout still supplies the same shared client state. /about is a sibling route, not a descendant of the Books layout, so it remains outside this feature scope.

One provider, two routed views

A component-tree diagram showing one provider in the Books layout around both the summary and changing routed content, while About is outside the Books branch.

The layout is the smallest shared subtree that contains both Books views.

Build the two route consumers

Build the route consumers

Create src/features/books/BookViews.tsx with the complete source below. These consumers use the checked Hook rather than receiving shortlist props through route components. The list and shortlist route can therefore read and update the same provider-owned state.

BookViews.tsx

src/features/books/BookViews.tsx

tsx
import { Link } from "react-router";
import { books } from "./books";
import { useShortlist } from "./useShortlist";

export function BooksView() {
  const { state, dispatch } = useShortlist();

  return (
    <section>
      <h1>Books</h1>
      <ul>
        {books.map((book) => {
          const selected = state.selectedIds.includes(book.id);

          return (
            <li key={book.id}>
              {book.title}{" "}
              <button
                type="button"
                onClick={() =>
                  dispatch({ type: "bookToggled", bookId: book.id })
                }
              >
                {selected ? "Remove from shortlist" : "Add to shortlist"}
              </button>
            </li>
          );
        })}
      </ul>
      <Link to="shortlist">Shortlist</Link>
    </section>
  );
}

export function ShortlistView() {
  const { state, dispatch } = useShortlist();
  const selectedBooks = books.filter((book) =>
    state.selectedIds.includes(book.id),
  );

  return (
    <section>
      <h1>Shortlist</h1>
      {selectedBooks.length === 0 ? (
        <p>No books selected.</p>
      ) : (
        <ul>
          {selectedBooks.map((book) => (
            <li key={book.id}>
              {book.title}{" "}
              <button
                type="button"
                onClick={() =>
                  dispatch({ type: "bookToggled", bookId: book.id })
                }
              >
                Remove from shortlist
              </button>
            </li>
          ))}
        </ul>
      )}
      <button
        type="button"
        onClick={() => dispatch({ type: "selectionCleared" })}
      >
        Clear shortlist
      </button>{" "}
      <Link to="/books">Books</Link>
    </section>
  );
}

export function ShortlistSummary() {
  const { state } = useShortlist();

  return <p>Shortlist: {state.selectedIds.length}</p>;
}

Connect layout, routes, and mounting

Compose the runnable routed feature

Add the shared layout, route composition, and entry point below. BooksLayout composes the existing provider around the summary and Outlet; it does not create another reducer.

After saving, run npm run dev. Vite prints a local origin such as http://localhost:5173. Open that origin followed by /books—for example, http://localhost:5173/books—because the application routes start at /books.

BooksLayout.tsx

src/features/books/BooksLayout.tsx

tsx
import { NavLink, Outlet } from "react-router";
import { ShortlistSummary } from "./BookViews";
import { ShortlistProvider } from "./ShortlistProvider";

export function BooksLayout() {
  return (
    <ShortlistProvider>
      <nav aria-label="Books navigation">
        <NavLink to="/books" end>
          Books
        </NavLink>{" "}
        <NavLink to="/books/shortlist">
          Shortlist
        </NavLink>{" "}
        <NavLink to="/about">About</NavLink>
      </nav>
      <ShortlistSummary />
      <Outlet />
    </ShortlistProvider>
  );
}

App.tsx

src/App.tsx

tsx
import { Link, Route, Routes } from "react-router";
import { BooksLayout } from "./features/books/BooksLayout";
import {
  BooksView,
  ShortlistView,
} from "./features/books/BookViews";

function AboutView() {
  return (
    <section>
      <h1>About</h1>
      <Link to="/books">Books</Link>
    </section>
  );
}

export default function App() {
  return (
    <Routes>
      <Route path="/books" element={<BooksLayout />}>
        <Route index element={<BooksView />} />
        <Route path="shortlist" element={<ShortlistView />} />
      </Route>
      <Route path="/about" element={<AboutView />} />
    </Routes>
  );
}

main.tsx

src/main.tsx

tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App";

const rootElement = document.getElementById("root");

if (rootElement === null) {
  throw new Error("Root element not found.");
}

createRoot(rootElement).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
);

Expected shared selection

A side-by-side comparison of the Books and Shortlist screens, each showing a shortlist count of one and Practical CSS as selected.

Changing the route changes the outlet content, not the provider instance above it. Both screens therefore reflect one shared selection.

Apply and verify provider placement

Enclose the shared descendants

Complete the JSX boundary that encloses both shared descendants.

tsx
export function BooksLayout() {
  return (
    
      <ShortlistSummary />
      <Outlet />
    
  );
}

Verify the shared selection

On your computer, open the Vite origin at /books. Add exactly one book in Books, then navigate to Shortlist. Report the shortlist count and book title you observed, and state whether the summary agreed with the selected-book view.

Write at least 30 characters (0/30).

Step 6 of 8

Observe the Provider's State Lifetime

Predict and test when the Books shortlist remains in memory and when a provider remount initializes it again.

What keeps the shortlist alive

State belongs to a mounted provider instance

A provider instance owns the reducer state created by useReducer. Its context value is available only while that specific instance stays mounted.

An ordinary re-render does not create a new provider instance, so the shortlist remains. Changing between child routes under BooksLayout also keeps the layout—and its provider instance—mounted.

State initializes again when React removes that provider or deliberately gives it a new identity with a different key.

Provider lifetime in the route tree

Diagram showing one provider remaining around changing Books route views, disappearing when About replaces the Books layout, and being replaced when its key changes.

The changing route view is not the lifetime boundary. The mounted provider instance is.

Predict the lifetime boundary

Lifetime timeline

Use the component tree, not the URL alone, to predict the shortlist. A URL change can preserve state when the same feature layout remains mounted.

Navigation and identity outcomes

Each row describes what happens to the provider instance that owns the shared client state.

A new provider instance starts from `initialShortlistState`.
ChangeProvider instanceShortlist result
Books to ShortlistSame instance remains mountedRetained
Ordinary BooksLayout re-renderSame stable identityRetained
Books to AboutBooks provider unmountsRemoved from memory
About back to BooksNew instance mountsInitialized again
Change provider keyOld instance is replacedInitialized again
Full page reloadNew JavaScript runtime and instanceInitialized again

Tip

Keep the boundary explicit

This is in-memory shared client state, not durable persistence. Reloading the page initializes this example again. Storage-backed restoration is a separate design decision.

Make remounting visible

Add a deliberate remount control

Replace src/features/books/BooksLayout.tsx with this temporary remount demonstration. The counter is outside ShortlistProvider, so clicking the button can change the provider’s identity. The provider still wraps both ShortlistSummary and Outlet.

Keep the About navigation link: use it for client-side navigation when testing the feature boundary.

src/features/books/BooksLayout.tsx

This file edit is not run by itself. Save it, then use your existing Vite development server to test the route journey.

tsx
import { useState } from "react";
import { NavLink, Outlet } from "react-router";
import { ShortlistSummary } from "./BookViews";
import { ShortlistProvider } from "./ShortlistProvider";

export function BooksLayout() {
  const [resetVersion, setResetVersion] = useState(0);

  return (
    <section>
      <nav aria-label="Books navigation">
        <NavLink to="/books" end>
          Books
        </NavLink>{" "}
        <NavLink to="/books/shortlist">
          Shortlist
        </NavLink>{" "}
        <NavLink to="/about">
          About
        </NavLink>
      </nav>

      <button
        type="button"
        onClick={() => setResetVersion((version) => version + 1)}
      >
        Remount shortlist
      </button>

      <ShortlistProvider key={resetVersion}>
        <ShortlistSummary />
        <Outlet />
      </ShortlistProvider>
    </section>
  );
}

Tip

Why the key changes the result

resetVersion is state owned by BooksLayout, not by the provider it resets. When its value changes, key={resetVersion} identifies a replacement provider instance. That replacement runs useReducer(shortlistReducer, initialShortlistState) again.

Observe the three lifetime outcomes

Verify the provider lifetime locally

Run the app and use its navigation links; do not reload the page or type a new address for these transitions.

  1. In Books, select Practical CSS, then navigate to Shortlist.
  2. Use About, then use the Books link in the About view to return to the feature.
  3. Select any book again, then click Remount shortlist.

Report the shortlist result after each numbered event and identify whether the relevant provider instance was retained, unmounted, or replaced.

Write at least 140 characters (0/140).

Step 7 of 8

Keep Unrelated State Out

Assign shared selection, temporary interaction details, addressable filters, and derived values to focused owners.

Choose the smallest useful owner

Share only the selection

A context contract should hold only state that genuinely needs to be shared by several descendants of its provider instance. In this feature, the selected book IDs are shared client state: the Books view changes them, while the shortlist view and summary read them.

That does not make the context a default destination for every value used near Books.

Different owners at a glance

Diagram showing a shared selection inside the Books provider, a help panel local to one view, and a browser URL filter entering the Books view.

The selection crosses related Books views. The help panel and the addressable filter have narrower or different owners.

Avoid duplicate sources of truth

Ownership map

Use the authoritative source that matches each requirement. Avoid duplicate fields that must stay synchronized.

Each value has one appropriate source of truth.
RequirementSource of truthScope and reason
Selected book IDsShortlistContext stateShared by Books descendants
Help panel expanded stateHelp-panel component stateOnly that interaction needs it
Addressable q filterURL query parameterA link and browser navigation can represent it
Selected-book countDerived from selectedIdsComputed while rendering; no duplicate field
Remote-resource freshnessOutside this contextHas separate data concerns

Avoid mirrored state

For example, compute a summary count from state.selectedIds.length. Storing both selectedIds and a separate count in ShortlistContextValue creates two values that could disagree.

Likewise, keep q authoritative in the URL. Copying it into shortlist context would create a second, competing source of truth.

Tip

Keep the boundary focused

This context is for client-owned selection within the Books feature. Remote-resource freshness and caching have separate concerns and are not added to this context contract.

Practice: preserve the boundary

Assign each value

Match each requirement to its appropriate owner.

Tap an item and then its matching pair.

Step 8 of 8

Apply and Verify the Scoped Design

Repair and verify a feature-scoped context design that shares a Books shortlist across related routes while preserving clear ownership and lifetime boundaries.

Find the ownership mistake

Find the ownership mistake

Diagnose the split-owner mistake

The Books feature needs one provider instance above both routed views. A faulty integration can look reasonable when each view is wrapped separately, but each wrapper mounts its own reducer state. A selection made in one view is therefore unavailable to the other.

The repair is structural: the parent /books route must render the existing shared layout, whose provider stays above changing nested route content. The /about route remains outside that feature boundary.

Incorrect: one provider per view

These are only the faulty route wrappers. Each wrapper creates a separate provider instance.

tsx
function BooksRoute() {
  return (
    <ShortlistProvider>
      <BooksView />
    </ShortlistProvider>
  );
}

function ShortlistRoute() {
  return (
    <ShortlistProvider>
      <ShortlistView />
    </ShortlistProvider>
  );
}

One owner versus separate owners

A comparison between two isolated provider instances and one provider instance above both Books route views.

One provider above changing route content gives both Books views the same reducer-owned shared client state. About remains outside the feature scope.

Place the provider at the shared boundary

Repair the shared route boundary

In src/App.tsx, replace the entire existing <Routes>...</Routes> block inside the default App export with the block below. Preserve the existing imports and the existing AboutView declaration.

tsx
<Routes>
  <Route path="/books" element={< />}>
    <Route index element={<BooksView />} />
    <Route path="shortlist" element={<ShortlistView />} />
  </Route>
  <Route path="/about" element={<AboutView />} />
</Routes>

Verify behavior in the browser

Verify the feature locally

Run the final local journey

Use the existing local app and the temporary Remount shortlist control from the previous step. Run npm run dev, open /books, and use the application links for every navigation change. Do not reload the page during this journey.

This verifies the implemented shortlist behavior. It does not require you to implement a q filter.

Verification journey

Perform these actions in order. Observe the summary and visible route content after each action.

The journey checks one shared provider instance, deliberate remounting, and leaving the Books feature.
ActionExpected observation
At `/books`, add Practical CSSThe summary shows one selected item.
Use Shortlist navigationPractical CSS is listed and the summary still shows one.
Remove Practical CSS in ShortlistThe summary returns to zero.
Return to Books, add TypeScript Patterns, then choose Remount shortlistThe replacement provider starts with an empty shortlist.
Add React Interfaces, use About, then return through the Books linkRe-entering Books shows an empty shortlist.

Explain the scoped design

Apply the complete design

After completing the repair and local journey, write 5–8 sentences that explain the complete scoped design. Include all of the following:

  • why the repaired parent route gives Books and Shortlist one shared client state;
  • one observation that verifies a selection survived navigation inside the Books feature;
  • why Remount shortlist clears the selection, and why visiting About then returning also initializes it again;
  • why a view-only help panel remains component-local;
  • why an addressable q filter would remain authoritative in URL state rather than be copied into ShortlistContext.

Do not add a filter for this response; assess q as an ownership decision only.

Write at least 420 characters (0/420).

Capability achieved

Summary

What you can now design

  • Context distributes a supplied value; ShortlistProvider remains the reducer state owner.
  • The checked useShortlist Hook gives Books descendants one typed state-and-dispatch contract and reports incorrect placement clearly.
  • One provider instance in the Books layout is shared by the summary, /books, and /books/shortlist.
  • Internal Books navigation retains that instance; a changed key, leaving the feature, or a page reload starts a new in-memory lifetime.
  • Keep view-only interaction details local, derive the count from selectedIds, and keep an addressable q filter in URL state.

Tutorial completed

Congratulations! You completed: Share Scoped Client State with Context

Completed: you can scope reducer-backed context to a routed feature, verify its lifetime, and keep unrelated state with the appropriate owner.

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate