Suomi

Tutustu React Suspenseen tiedonhaussa koodin jakamisen ohi. Opi Fetch-As-You-Render, virheenkäsittely ja kestäviä tiedonhakumalleja globaaleille sovelluksille.

React Suspense -resurssien lataaminen: Nykyaikaisten tiedonhakumallien hallinta

Verkkokehityksen dynaamisessa maailmassa käyttökokemus (UX) hallitsee. Sovellusten odotetaan olevan nopeita, responsiivisia ja miellyttäviä, riippumatta verkon olosuhteista tai laitteen ominaisuuksista. React-kehittäjille tämä tarkoittaa usein monimutkaista tilanhallintaa, monimutkaisia latausindikaattoreita ja jatkuvaa taistelua tiedonhaun vesiputouksia vastaan. Tässä kohtaa astuu esiin React Suspense, tehokas, vaikkakin usein väärinymmärretty, ominaisuus, joka on suunniteltu mullistamaan tapaamme käsitellä asynkronisia toimintoja, erityisesti tiedonhakua.

Alun perin koodin jakamiseen React.lazy():n avulla esitelty Suspensen todellinen potentiaali piilee sen kyvyssä orkestroida *minkä tahansa* asynkronisen resurssin lataamista, mukaan lukien data API:sta. Tämä kattava opas sukeltaa syvälle React Suspenseen resurssien latauksessa, tutkien sen ydinkonsepteja, perustavanlaatuisia tiedonhakumalleja ja käytännön näkökohtia tehokkaiden ja kestävien globaalien sovellusten rakentamisessa.

Tiedonhaun kehitys Reactissa: Imperatiivisesta deklaratiiviseen

Monien vuosien ajan tiedonhaku React-komponenteissa perustui pääasiassa yleiseen malliin: käytettiin useEffect-hookia API-kutsujen käynnistämiseen, hallittiin lataus- ja virhetiloja useState-hookin avulla ja suoritettiin ehdollinen renderöinti näiden tilojen perusteella. Vaikka tämä lähestymistapa oli toimiva, se johti usein useisiin haasteisiin:

Consider a typical data fetching scenario without Suspense:

import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setIsLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        setUser(data);
      } catch (e) {
        setError(e);
      } finally {
        setIsLoading(false);
      }
    };
    fetchUser();
  }, [userId]);

  if (isLoading) {
    return <p>Ladataan käyttäjäprofiilia...</p>;
  }

  if (error) {
    return <p style={"color: red;"}>Virhe: {error.message}</p>;
  }

  if (!user) {
    return <p>Käyttäjätietoja ei saatavilla.</p>;
  }

  return (
    <div>
      <h2>Käyttäjä: {user.name}</h2>
      <p>Sähköposti: {user.email}</p>
      <!-- More user details -->
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Tervetuloa sovellukseen</h1>
      <UserProfile userId={\"123\"} />
    </div>
  );
}

This pattern is ubiquitous, but it forces the component to manage its own asynchronous state, often leading to a tightly coupled relationship between the UI and the data fetching logic. Suspense offers a more declarative and streamlined alternative.

React Suspensen ymmärtäminen koodin jakamisen ulkopuolella

Useimmat kehittäjät kohtaavat Suspensen ensin React.lazy():n kautta koodin jakamista varten, jossa se antaa mahdollisuuden lykätä komponentin koodin lataamista, kunnes sitä tarvitaan. Esimerkiksi:

import React, { Suspense, lazy } from 'react';

const LazyComponent = lazy(() => import('./MyHeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Ladataan komponenttia...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

Tässä skenaariossa, jos MyHeavyComponent ei ole vielä latautunut, <Suspense>-raja nappaa lazy():n heittämän lupauksen ja näyttää fallback-sisällön, kunnes komponentin koodi on valmis. Tärkein oivallus tässä on, että Suspense toimii nappaamalla lupaukset, jotka heitetään renderöinnin aikana.

Tämä mekanismi ei ole yksinomainen koodin lataamiselle. Mikä tahansa renderöinnin aikana kutsuttu funktio, joka heittää lupauksen (esim. koska resurssi ei ole vielä saatavilla), voidaan napata komponenttipuun ylempänä olevalla Suspense-rajalla. Kun lupaus ratkeaa, React yrittää renderöidä komponentin uudelleen, ja jos resurssi on nyt saatavilla, varalataus piilotetaan ja todellinen sisältö näytetään.

Suspensen ydinkonseptit tiedonhakuun

Jotta Suspensea voitaisiin hyödyntää tiedonhakuun, meidän on ymmärrettävä muutama ydinkäsitys:

1. Lupauksen heittäminen

Toisin kuin perinteinen asynkroninen koodi, joka käyttää async/awaitia lupausten ratkaisemiseen, Suspense perustuu funktioon, joka *heittää* lupauksen, jos data ei ole valmis. Kun React yrittää renderöidä komponentin, joka kutsuu tällaista funktiota, ja data on edelleen odottavassa tilassa, lupaus heitetään. React sitten 'pysäyttää' kyseisen komponentin ja sen lasten renderöinnin etsien lähintä <Suspense>-rajaa.

2. Suspense-raja

<Suspense>-komponentti toimii virherajana lupauksille. Se ottaa fallback-propin, joka on käyttöliittymä, joka renderöidään, kun jokin sen lapsista (tai niiden jälkeläisistä) keskeyttää (eli heittää lupauksen). Kun kaikki sen alipuun sisällä heitetyt lupaukset ovat ratkenneet, varalataus korvataan todellisella sisällöllä.

Yksi Suspense-raja voi hallita useita asynkronisia operaatioita. Esimerkiksi jos sinulla on kaksi komponenttia saman <Suspense>-rajan sisällä, ja kummankin on haettava tietoja, varalataus näkyy, kunnes *molemmat* tiedonhaut ovat valmiita. Tämä estää osittaisen käyttöliittymän näyttämisen ja tarjoaa koordinoidumman latauskokemuksen.

3. Välimuisti/resurssienhallinta (käyttäjän vastuulla)

Tärkeää on, että Suspense itse ei hoida tiedonhakua tai välimuistitusta. Se on vain koordinointimekanismi. Jotta Suspense toimisi tiedonhaussa, tarvitset kerroksen, joka:

Tämä 'resurssienhallinta' toteutetaan tyypillisesti yksinkertaisella välimuistilla (esim. Map tai objekti) kunkin resurssin tilan tallentamiseen (odottaa, ratkaistu tai virheellinen). Vaikka voit rakentaa tämän manuaalisesti esittelytarkoituksiin, todellisessa sovelluksessa käyttäisit vankkaa tiedonhakukirjastoa, joka integroituu Suspenseen.

4. Rinnakkainen tila (React 18:n parannukset)

Vaikka Suspensea voidaan käyttää Reactin vanhemmissa versioissa, sen täysi teho vapautuu rinnakkaisen Reactin (oletuksena käytössä React 18:ssa createRootin kanssa) kanssa. Rinnakkainen tila antaa Reactille mahdollisuuden keskeyttää, pysäyttää ja jatkaa renderöintityötä. Tämä tarkoittaa:

Tiedonhakumallit Suspensen avulla

Tutustutaan tiedonhakumallien kehitykseen Suspensen myötä.

Malli 1: Hae-sitten-renderöi (perinteinen Suspense-kääreellä)

Tämä on klassinen lähestymistapa, jossa data haetaan, ja vasta sitten komponentti renderöidään. Vaikka se ei hyödynnä suoraan 'heittää lupauksen' -mekanismia datalle, voit kääriä komponentin, joka *lopulta* renderöi dataa, Suspense-rajalla tarjotaksesi varalatauksen. Tämä koskee enemmän Suspensen käyttöä yleisenä latauskäyttöliittymän orkestraattorina komponenteille, jotka lopulta ovat valmiina, vaikka niiden sisäinen tiedonhaku perustuu edelleen perinteiseen useEffect-malliin.

import React, { Suspense, useState, useEffect } from 'react';

function UserDetails({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetchUserData = async () => {
      setIsLoading(true);
      const res = await fetch(`/api/users/${userId}`);
      const data = await res = await res.json();
      setUser(data);
      setIsLoading(false);
    };
    fetchUserData();
  }, [userId]);

  if (isLoading) {
    return <p>Ladataan käyttäjän tietoja...</p>;
  }

  return (
    <div>
      <h3>Käyttäjä: {user.name}</h3>
      <p>Sähköposti: {user.email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Hae-sitten-renderöi Esimerkki</h1>
      <Suspense fallback={<div>Koko sivua ladataan...</div>}>
        <UserDetails userId={\"1\"} />
      </Suspense>
    </div>
  );
}

Hyödyt: Yksinkertainen ymmärtää, taaksepäin yhteensopiva. Voidaan käyttää nopeana tapana lisätä globaali lataustila.

Haitat: Ei poista boilerplate-koodia UserDetails-komponentin sisältä. Edelleen altis vesiputouksille, jos komponentit hakevat tietoja peräkkäin. Ei todella hyödynnä Suspensen 'heittää-ja-napata' -mekanismia itse datalle.

Malli 2: Renderöi-sitten-hae (haku renderöinnin sisällä, ei tuotantokäyttöön)

Tämä malli on ensisijaisesti havainnollistamaan, mitä ei tulisi tehdä Suspensen kanssa suoraan, sillä se voi johtaa äärettömiin silmukoihin tai suorituskykyongelmiin, jos sitä ei käsitellä huolellisesti. Se sisältää yrityksen hakea dataa tai kutsua keskeyttävää funktiota suoraan komponentin renderöintivaiheessa, *ilman* asianmukaista välimuistitusmekanismia.

// DO NOT USE THIS IN PRODUCTION WITHOUT A PROPER CACHING LAYER
// This is purely for illustration of how a direct 'throw' might work conceptually.

let fetchedData = null;
let dataPromise = null;

function fetchDataSynchronously(url) {
  if (fetchedData) {
    return fetchedData;
  }

  if (!dataPromise) {
    dataPromise = fetch(url)
      .then(res => res.json())
      .then(data => { fetchedData = data; dataPromise = null; return data; })
      .catch(err => { dataPromise = null; throw err; });
  }
  throw dataPromise; // This is where Suspense kicks in
}

function UserDetailsBadExample({ userId }) {
  const user = fetchDataSynchronously(`/api/users/${userId}`);
  return (
    <div>
      <h3>Käyttäjä: {user.name}</h3>
      <p>Sähköposti: {user.email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Renderöi-sitten-hae (Havainnollistava, EI Suositella Suoraan)</h1>
      <Suspense fallback={<div>Ladataan käyttäjää...</div>}>
        <UserDetailsBadExample userId={\"2\"} />
      </Suspense>
    </div>
  );
}

Hyödyt: Osoittaa, kuinka komponentti voi suoraan 'pyytää' dataa ja keskeyttää, jos se ei ole valmis.

Haitat: Erittäin ongelmallinen tuotantoon. Tämä manuaalinen, globaali fetchedData ja dataPromise -järjestelmä on yksinkertaistettu, eikä se käsittele useita pyyntöjä, mitätöintiä tai virhetiloja vankasti. Se on primitiivinen havainnollistus 'heittää-lupaus' -konseptista, ei malli, joka kannattaa ottaa käyttöön.

Malli 3: Hae-samalla-kun-renderöit (ihanteellinen Suspense-malli)

Tämä on paradigman muutos, jonka Suspense todella mahdollistaa tiedonhakuun. Sen sijaan, että odotettaisiin komponentin renderöitymistä ennen datan hakua, tai haettaisiin kaikki data etukäteen, Hae-samalla-kun-renderöit tarkoittaa, että datan haku aloitetaan *mahdollisimman pian*, usein *ennen* tai *samanaikaisesti* renderöintiprosessin kanssa. Komponentit sitten 'lukevat' dataa välimuistista, ja jos data ei ole valmis, ne keskeyttävät. Ydinidea on erottaa tiedonhaku logiikka komponentin renderöintilogiikasta.

Hae-samalla-kun-renderöit -mallin toteuttamiseksi tarvitset mekanismin, joka:

  1. Käynnistää tiedonhaun komponentin renderöintifunktion ulkopuolella (esim. kun reitti syötetään tai painiketta napsautetaan).
  2. Tallentaa lupauksen tai ratkaistun datan välimuistiin.
  3. Tarjoaa tavan komponenteille 'lukea' tästä välimuistista. Jos data ei ole vielä saatavilla, lukufunktio heittää odottavan lupauksen.

Tämä malli ratkaisee vesiputousongelman. Jos kaksi eri komponenttia tarvitsee dataa, niiden pyynnöt voidaan käynnistää rinnakkain, ja käyttöliittymä ilmestyy vasta, kun *molemmat* ovat valmiina, yhden Suspense-rajan orkestroimana.

Manuaalinen toteutus (ymmärtämistä varten)

Perusmekaniikan ymmärtämiseksi luodaan yksinkertaistettu manuaalinen resurssienhallinta. Todellisessa sovelluksessa käyttäisit omaa kirjastoa.

import React, { Suspense } from 'react';

// --- Simple Cache/Resource Manager ---
const cache = new Map();

function createResource(promise) {
  let status = 'pending';
  let result;
  let suspender = promise.then(
    (r) => {
      status = 'success';
      result = r;
    },
    (e) => {
      status = 'error';
      result = e;
    }
  );

  return {
    read() {
      if (status === 'pending') {
        throw suspender;
      } else if (status === 'error') {
        throw result;
      } else if (status === 'success') {
        return result;
      }
    },
  };
}

function fetchData(key, fetcher) {
  if (!cache.has(key)) {
    cache.set(key, createResource(fetcher()));
  }
  return cache.get(key);
}

// --- Data Fetching Functions ---
const fetchUserById = (id) => {
  console.log(`Haetaan käyttäjää ${id}...`);
  return new Promise(resolve => setTimeout(() => {
    const users = {
      '1': { id: '1', name: 'Alice Smith', email: 'alice@example.com' },
      '2': { id: '2', name: 'Bob Johnson', email: 'bob@example.com' },
      '3': { id: '3', name: 'Charlie Brown', email: 'charlie@example.com' }
    };
    resolve(users[id]);
  }, 1500));
};

const fetchPostsByUserId = (userId) => {
  console.log(`Haetaan postauksia käyttäjälle ${userId}...`);
  return new Promise(resolve => setTimeout(() => {
    const posts = {
      '1': [{ id: 'p1', title: 'My First Post' }, { id: 'p2', title: 'Travel Adventures' }],
      '2': [{ id: 'p3', title: 'Coding Insights' }],
      '3': [{ id: 'p4', title: 'Global Trends' }, { id: 'p5', title: 'Local Cuisine' }]
    };
    resolve(posts[userId] || []);
  }, 2000));
};

// --- Components ---
function UserProfile({ userId }) {
  const userResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
  const user = userResource.read(); // This will suspend if user data is not ready

  return (
    <div>
      <h3>Käyttäjä: {user.name}</h3>
      <p>Sähköposti: {user.email}</p>
    </div>
  );
}

function UserPosts({ userId }) {
  const postsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
  const posts = postsResource.read(); // This will suspend if posts data is not ready

  return (
    <div>
      <h4>Käyttäjän {userId} postaukset:</h4>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
        {posts.length === 0 && <li>Postauksia ei löytynyt.</li>}
      </ul>
    </div>
  );
}

// --- Application ---
let initialUserResource = null;
let initialPostsResource = null;

function prefetchDataForUser(userId) {
  initialUserResource = fetchData(`user-${userId}`, () => fetchUserById(userId));
  initialPostsResource = fetchData(`posts-${userId}`, () => fetchPostsByUserId(userId));
}

// Pre-fetch some data before the App component even renders
prefetchDataForUser('1');

function App() {
  return (
    <div>
      <h1>Hae-samalla-kun-renderöit Suspensen avulla</h1>
      <p>Tämä osoittaa, kuinka tiedonhaku voi tapahtua rinnakkain, Suspensen koordinoimana.</p>

      <Suspense fallback={<div>Ladataan käyttäjäprofiilia ja postauksia...</div>}>
        <UserProfile userId={\"1\"} />
        <UserPosts userId={\"1\"} />
      </Suspense>

      <h2>Toinen osio</h2>
      <Suspense fallback={<div>Ladataan toista käyttäjää...</div>}>
        <UserProfile userId={\"2\"} />
      </Suspense>
    </div>
  );
}

In this example:

Kirjastot Hae-samalla-kun-renderöit -malliin

Vankan resurssienhallinnan rakentaminen ja ylläpitäminen manuaalisesti on monimutkaista. Onneksi useat kypsät tiedonhakukirjastot ovat ottaneet tai ottavat käyttöön Suspensen, tarjoten testattuja ratkaisuja:

Nämä kirjastot abstrahoivat resurssien luomisen ja hallinnan, välimuistituksen, uudelleenvalidoinnin, optimististen päivitysten ja virheenkäsittelyn monimutkaisuudet, mikä tekee Hae-samalla-kun-renderöit -mallin toteuttamisesta paljon helpompaa.

Malli 4: Esihaku Suspense-tietoisten kirjastojen avulla

Esihaku on tehokas optimointi, jossa haet proaktiivisesti dataa, jota käyttäjä todennäköisesti tarvitsee lähitulevaisuudessa, jo ennen kuin hän sitä eksplisiittisesti pyytää. Tämä voi parantaa merkittävästi koettua suorituskykyä.

Suspense-tietoisten kirjastojen avulla esihaku on saumatonta. Voit käynnistää tiedonhaut käyttäjän toiminnoilla, jotka eivät välittömästi muuta käyttöliittymää, kuten linkin päälle vieminen tai painikkeen hiiriyhteys.

import React, { Suspense } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

// Assume these are your API calls
const fetchProductById = async (id) => {
  console.log(`Haetaan tuotetta ${id}...`);
  return new Promise(resolve => setTimeout(() => {
    const products = {
      'A001': { id: 'A001', name: 'Global Widget X', price: 29.99, description: 'Monipuolinen widget kansainväliseen käyttöön.' },
      'B002': { id: 'B002', name: 'Universal Gadget Y', price: 149.99, description: 'Huippuluokan vempain, rakastettu maailmanlaajuisesti.' },
    };
    resolve(products[id]);
  }, 1000));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true, // Enable Suspense for all queries by default
    },
  },
});

function ProductDetails({ productId }) {
  const { data: product } = useQuery({
    queryKey: ['product', productId],
    queryFn: () => fetchProductById(productId),
  });

  return (
    <div style={{\"border\": \"1px solid #ccc\", \"padding\": \"15px\", \"margin\": \"10px 0\"}}>
      <h3>{product.name}</h3>
      <p>Hinta: ${product.price.toFixed(2)}</p>
      <p>{product.description}</p>
    </div>
  );
}

function ProductList() {
  const handleProductHover = (productId) => {
    // Prefetch data when a user hovers over a product link
    queryClient.prefetchQuery({
      queryKey: ['product', productId],
      queryFn: () => fetchProductById(productId),
    });
    console.log(`Prefetching product ${productId}`);
  };

  return (
    <div>
      <h2>Saatavilla olevat tuotteet:</h2>
      <ul>
        <li>
          <a href=\"#\" onMouseEnter={() => handleProductHover('A001')}
             onClick={(e) => { e.preventDefault(); /* Navigate or show details */ }}
          >Globaali Widget X (A001)</a>
        </li>
        <li>
          <a href=\"#\" onMouseEnter={() => handleProductHover('B002')}
             onClick={(e) => { e.preventDefault(); /* Navigate or show details */ }}
          >Universaali Gadget Y (B002)</a>
        </li>
      </ul>
      <p>Vie hiiri tuotelinkin päälle nähdäksesi esihakun toiminnassa. Avaa verkkonäkymä seurataksesi.</p>
    </div>
  );
}

function App() {
  const [showProductA, setShowProductA] = React.useState(false);
  const [showProductB, setShowProductB] = React.useState(false);

  return (
    <QueryClientProvider client={queryClient}>
      <h1>Esihaku React Suspensen avulla (React Query)</h1>
      <ProductList />

      <button onClick={() => setShowProductA(true)}>Näytä Globaali Widget X</button>
      <button onClick={() => setShowProductB(true)}>Näytä Universaali Gadget Y</button>

      {showProductA && (
        <Suspense fallback={<p>Ladataan Globaalia Widget X:ää...</p>}>
          <ProductDetails productId=\"A001\" />
        </Suspense>
      )}
      {showProductB && (
        <Suspense fallback={<p>Ladataan Universaalia Gadget Y:tä...</p>}>
          <ProductDetails productId=\"B002\" />
        </Suspense>
      )}
    </QueryClientProvider>
  );
}

In this example, hovering over a product link triggers `queryClient.prefetchQuery`, which initiates the data fetch in the background. If the user then clicks the button to show the product details, and the data is already in the cache from the prefetch, the component will render instantly without suspending. If the prefetch is still in progress or wasn't initiated, Suspense will display the fallback until the data is ready.

Virheenkäsittely Suspensen ja virherajojen avulla

Vaikka Suspense käsittelee 'lataus' -tilaa näyttämällä varalatauksen, se ei suoraan käsittele 'virhe' -tiloja. Jos keskeyttävän komponentin heittämä lupaus hylätään (eli tiedonhaku epäonnistuu), tämä virhe etenee komponenttipuussa ylöspäin. Näiden virheiden grasefulliseen käsittelyyn ja asianmukaisen käyttöliittymän näyttämiseen tarvitset Virherajoja (Error Boundaries).

Virheraja on React-komponentti, joka toteuttaa joko componentDidCatch- tai static getDerivedStateFromError -elinkaarimetodit. Se nappaa JavaScript-virheet mistä tahansa lapsikomponenttipuun osasta, mukaan lukien virheet, jotka ovat peräisin lupauksista, jotka Suspense normaalisti nappaisi, jos ne olisivat odottavassa tilassa.

import React, { Suspense, useState } from 'react';
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

// --- Error Boundary Component ---
class MyErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    console.error(\"Caught an error:\", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return (
        <div style={{\"border\": \"2px solid red\", \"padding\": \"20px\", \"margin\": \"20px 0\", \"background\": \"#ffe0e0\"}}>
          <h2>Jotain meni pieleen!</h2>
          <p>{this.state.error && this.state.error.message}</p>
          <p>Yritä päivittää sivu tai ota yhteyttä tukeen.</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>Yritä uudelleen</button>
        </div>
      );
    }
    return this.props.children;
  }
}

// --- Data Fetching (with potential for error) ---
const fetchItemById = async (id) => {
  console.log(`Yritetään hakea kohdetta ${id}...`);
  return new Promise((resolve, reject) => setTimeout(() => {
    if (id === 'error-item') {
      reject(new Error('Kohteen lataaminen epäonnistui: Verkkoon ei saada yhteyttä tai kohdetta ei löydy.'));
    } else if (id === 'slow-item') {
      resolve({ id: 'slow-item', name: 'Toimitettu hitaasti', data: 'Tämä kohde kesti hetken, mutta saapui!', status: 'success' });
    } else {
      resolve({ id, name: `Kohde ${id}`, data: `Tiedot kohteelle ${id}` });
    }
  }, id === 'slow-item' ? 3000 : 800));
};

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
      retry: false, // For demonstration, disable retry so error is immediate
    },
  },
});

function DisplayItem({ itemId }) {
  const { data: item } = useQuery({
    queryKey: ['item', itemId],
    queryFn: () => fetchItemById(itemId),
  });

  return (
    <div>
      <h3>Kohteen tiedot:</h3>
      <p>ID: {item.id}</p>
      <p>Nimi: {item.name}</p>
      <p>Data: {item.data}</p>
    </div>
  );
}

function App() {
  const [fetchType, setFetchType] = useState('normal-item');

  return (
    <QueryClientProvider client={queryClient}>
      <h1>Suspense ja virherajat</h1>

      <div>
        <button onClick={() => setFetchType('normal-item')}>Hae normaali kohde</button>
        <button onClick={() => setFetchType('slow-item')}>Hae hidas kohde</button>
        <button onClick={() => setFetchType('error-item')}>Hae virheellinen kohde</button>
      </div>

      <MyErrorBoundary>
        <Suspense fallback={<p>Ladataan kohdetta Suspensen kautta...</p>}>
          <DisplayItem itemId={fetchType} />
        </Suspense>
      </MyErrorBoundary>
    </QueryClientProvider>
  );
}

By wrapping your Suspense boundary (or the components that might suspend) with an Error Boundary, you ensure that network failures or server errors during data fetching are caught and handled gracefully, preventing the entire application from crashing. This provides a robust and user-friendly experience, allowing users to understand the issue and potentially retry.

Tilanhallinta ja datan mitätöinti Suspensen avulla

On tärkeää selventää, että React Suspense käsittelee ensisijaisesti asynkronisten resurssien alkuperäistä lataustilaa. Se ei luonnostaan hallitse asiakaspään välimuistia, käsittele datan mitätöintiä tai orkestroi mutaatioita (luo, päivitä, poista -operaatioita) ja niiden myöhempiä käyttöliittymäpäivityksiä.

Tässä kohtaa Suspense-tietoiset tiedonhakukirjastot (React Query, SWR, Apollo Client, Relay) ovat välttämättömiä. Ne täydentävät Suspensea tarjoamalla:

Ilman vankkaa tiedonhakukirjastoa näiden ominaisuuksien toteuttaminen manuaalisen Suspense-resurssienhallinnan päälle olisi merkittävä urakka, joka edellyttäisi käytännössä oman tiedonhakukehyksen rakentamista.

Käytännön huomioita ja parhaita käytäntöjä

Suspensen käyttöönotto tiedonhakuun on merkittävä arkkitehtoninen päätös. Tässä joitakin käytännön huomioita globaalille sovellukselle:

1. Kaikki data ei tarvitse Suspensea

Suspense sopii ihanteellisesti kriittiselle datalle, joka vaikuttaa suoraan komponentin alkuperäiseen renderöintiin. Ei-kriittiselle datalle, taustahauille tai datalle, joka voidaan ladata laiskasti ilman suurta visuaalista vaikutusta, perinteinen useEffect tai esi-renderöinti voi edelleen olla sopiva. Suspensen liiallinen käyttö voi johtaa vähemmän hienorakeiseen latauskokemukseen, koska yksi Suspense-raja odottaa *kaikkien* lastensa ratkeamista.

2. Suspense-rajojen hienorakeisuus

Sijoita <Suspense>-rajat harkitusti. Yksi suuri raja sovelluksen yläosassa saattaa piilottaa koko sivun pyörivän kuvakkeen taakse, mikä voi olla turhauttavaa. Pienemmät, hienorakeisemmat rajat antavat sivun eri osien latautua itsenäisesti, tarjoten progressiivisemman ja responsiivisemman kokemuksen. Esimerkiksi raja käyttäjäprofiilikomponentin ympärillä ja toinen suositeltujen tuotteiden luettelon ympärillä.

<div>
  <h1>Tuotesivu</h1>
  <Suspense fallback={<p>Ladataan päätuotetietoja...</p>}>
    <ProductDetails id=\"prod123\" />
  </Suspense>

  <hr />

  <h2>Aiheeseen liittyvät tuotteet</h2>
  <Suspense fallback={<p>Ladataan aiheeseen liittyviä tuotteita...</p>}>
    <RelatedProducts category=\"electronics\" />
  </Suspense>
</div>

This approach means users can see the main product details even if the related products are still loading.

3. Palvelinpuolen renderöinti (SSR) ja HTML:n suoratoisto

React 18:n uudet suoratoisto-SSR-rajapinnat (renderToPipeableStream) integroituvat täysin Suspenseen. Tämä antaa palvelimen lähettää HTML:ää heti, kun se on valmis, vaikka osat sivusta (kuten datasta riippuvaiset komponentit) ovat edelleen latautumassa. Palvelin voi suoratoistaa paikkamerkin (Suspense-varalatauksesta) ja sitten suoratoistaa todellisen sisällön, kun data ratkeaa, ilman että koko asiakaspään uudelleenrenderöintiä tarvitaan. Tämä parantaa merkittävästi koettua lataussuorituskykyä globaaleille käyttäjille vaihtelevissa verkon olosuhteissa.

4. Inkrementaalinen käyttöönotto

Sinun ei tarvitse kirjoittaa koko sovellustasi uudelleen käyttääksesi Suspensea. Voit ottaa sen käyttöön vaiheittain, aloittaen uusista ominaisuuksista tai komponenteista, jotka hyötyisivät eniten sen deklaratiivisista latausmalleista.

5. Työkalut ja virheenkorjaus

Vaikka Suspense yksinkertaistaa komponenttien logiikkaa, virheenkorjaus voi olla erilaista. React DevTools tarjoaa tietoa Suspense-rajoista ja niiden tiloista. Tutustu siihen, miten valitsemasi tiedonhakukirjasto paljastaa sisäisen tilansa (esim. React Query Devtools).

6. Suspense-varalatausten aikakatkaisut

Hyvin pitkien latausaikojen tapauksessa saatat haluta ottaa käyttöön aikakatkaisun Suspense-varalataukseen tai vaihtaa yksityiskohtaisempaan latausindikaattoriin tietyn viiveen jälkeen. React 18:n useDeferredValue- ja useTransition-hookit voivat auttaa näiden vivahteikkaampien lataustilojen hallinnassa, jolloin voit näyttää 'vanhan' version käyttöliittymästä samalla kun uutta dataa haetaan, tai lykätä ei-kiireellisiä päivityksiä.

Tiedonhaun tulevaisuus Reactissa: React Server Components ja sen ulkopuolella

Tiedonhaun matka Reactissa ei pääty asiakaspään Suspenseen. React Server Components (RSC) edustaa merkittävää kehitystä, luvaten hämärtää rajat asiakkaan ja palvelimen välillä ja optimoida tiedonhakua edelleen.

Reactin kypsyessä Suspense tulee olemaan yhä keskeisempi osa palapelissä erittäin tehokkaiden, käyttäjäystävällisten ja ylläpidettävien sovellusten rakentamisessa. Se ajaa kehittäjiä kohti deklaratiivisempaa ja kestävämpää tapaa käsitellä asynkronisia toimintoja, siirtäen monimutkaisuuden yksittäisistä komponenteista hyvin hallittuun datakerrokseen.

Yhteenveto

React Suspense, alun perin ominaisuus koodin jakamiseen, on kehittynyt transformatiiviseksi työkaluksi tiedonhakuun. Ottamalla käyttöön Hae-samalla-kun-renderöit -mallin ja hyödyntämällä Suspense-tietoisia kirjastoja, kehittäjät voivat merkittävästi parantaa sovellustensa käyttökokemusta, poistamalla latausvesiputouksia, yksinkertaistamalla komponenttilogiikkaa ja tarjoamalla sujuvia, koordinoituja lataustiloja. Yhdistettynä virherajoihin vankkaa virheenkäsittelyä varten ja React Server Componentsin tulevaisuuden lupaukseen, Suspense antaa meille mahdollisuuden rakentaa sovelluksia, jotka ovat paitsi tehokkaita ja kestäviä, myös luonnostaan miellyttävämpiä käyttäjille ympäri maailmaa. Siirtyminen Suspense-ohjattuun tiedonhakuun vaatii käsitteellistä säätöä, mutta edut koodin selkeyden, suorituskyvyn ja käyttäjätyytyväisyyden kannalta ovat huomattavat ja investoinnin arvoiset.