Research
Security News
Threat Actor Exposes Playbook for Exploiting npm to Build Blockchain-Powered Botnets
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.
react-streaming
Advanced tools
react-streaming
React 18 Streaming. Full-fledged & Easy.
Unfamiliar with React Streaming? Check out Dan's article about SSR and Streaming.
Follow: Twitter > @brillout
Chat: Discord > Vikereact-streaming
Contents
Features (for React users):
<Suspense>
for SSR apps.useAsync()
: easily fetch data for SSR apps.conservative
or google-speed
.Features (for library authors):
useAsync()
: add data fetching capabilities to your library. High-level and easy to use.injectToStream()
: inject chunks to the stream for your library. Low-level and difficult to use, but highly flexible.Easy:
import { renderToStream } from 'react-streaming/server'
const {
pipe, // Node.js (Vercel, AWS EC2, ...)
readable // Edge (Coudflare Workers, Deno Deploy, Netlify Edge, Vercel Edge, ...)
} = await renderToStream(<Page />)
⚠️ While
react-streaming
is stable in itself (it's used in production and has good CI test coverage), note that React's SSR streaming support is still early and that the React team is working on high-level APIs that may make parts ofreact-streaming
obsolete, see React RFC injectToStream - @sebmarkbage comment.
React 18's new SSR streaming architecture unlocks many capabilities:
The problem: the current React 18 Streaming architecture is low-level and difficult to use.
The solution: react-streaming
.
react-streaming
makes it easy to build the libraries of tomorrow, for example:
- Use Telefunc to easily fetch data for your Next.js app or your Vite +
vite-plugin-ssr
app. (Replacing Next.js'sgetServerSideProps()
andvite-plugin-ssr
'sonBeforeRender()
.)- Better GraphQL tools, e.g. Vilay.
Install
npm install react-streaming
Server-side
import { renderToStream } from 'react-streaming/server'
const {
pipe, // Defined if running in Node.js, otherwise `null`
readable // Defined if running on the Edge (.e.g. Coudflare Workers), otherwise `null`
} = await renderToStream(<Page />)
That's it.
const options = {
// ...
}
await renderToStream(<Page />, options)
options.disable?: boolean
: Disable streaming.
<Page>
is still rendered to a stream, but the promiseconst promise = renderToStream()
resolves only after the stream has finished. (This effectively disables streaming from a user perspective, while unlocking React 18 Streaming capabilities such as SSR<Supsense>
.)
options.seoStrategy?: 'conservative' | 'google-speed'
conservative
(default): Disable streaming if the HTTP request originates from a bot. (Ensuring bots to always see the whole HTML.)
google-speed
: Don't disable streaming for the Google Bot.
Custom SEO strategy: use options.disable
. For example:
// Always stream, even for bots:
const disable = false
// Disable streaming for bots, except for the Google Bot and some other bot:
const disable =
isBot(userAgent) &&
!['googlebot', 'some-other-bot'].some(n => userAgent.toLowerCase().includes(n))
await renderToStream(<Page />, { disable })
options.userAgent?: string
: The HTTP User-Agent request header. (Needed for options.seoStrategy
.)
options.webStream?: boolean
: Use Web Streams instead of Node.js Streams in Node.js. (Node.js 18 released Web Streams support.)
options.onBoundaryError?: (err: unknown) => void
: Called when a <Suspense>
boundary fails. See Error Handling.
const { streamEnd } = await renderToStream(<Page />)
// ✅ Page Shell succesfully rendered.
const success: boolean = await streamEnd
// Stream ended.
if (success) {
// ✅ <Page> succesfully rendered
} else {
// ❌ A <Suspense> boundary failed.
}
Note that streamEnd
never rejects.
⚠️ Read Error Handling before using
streamEnd
. In particular, do not usesuccess
to change the behavior of your app/stream (because React automatically takes care of gracefully handling<Suspense>
failures).
The promise await renderToStream()
resolves after the page shell is rendered. This means that if an error occurs while rendering the page shell, then the promise rejects with that error.
:book: The page shell is the set of all components before
<Suspense>
boundaries.
try {
await renderToStream(<Page />)
// ✅ Page shell succesfully rendered and is ready in the stream buffer.
} catch(err) {
// ❌ Something went wrong while rendering the page shell.
}
The stream returned by await renderToStream()
doesn't emit errors.
:book: If an error occurs during the stream, then that means that a
<Suspense>
boundary failed. Instead of emiting a stream error, React swallows the error on the server-side and retries to resolve the<Suspense>
boundary on the client-side. If the<Suspense>
fails again on the client-side, then the client-side throws the error.This means that errros occuring during the stream are handled by React and there is nothing for you to do on the server-side. That said, you may want to gracefully handle the error on the client-side e.g. with
react-error-boundary
.You can use
options.onBoundaryError()
for error tracking purposes.
useAsync()
import { useAsync } from 'react-streaming'
function Page({ movieId }) {
return (
<Suspense fallback={<p>Loading...</p>}>
<Movie id={movieId}/>
</Suspense>
)
}
async function fetchMovie(id) {
const response = await fetch(`https://star-wars.brillout.com/api/films/${id}.json`)
return response.json()
}
// This component is isomorphic: it works on both the client-side and server-side.
// The data fetched while SSR is automatically passed and re-used on the client for hydration.
function Movie({ id }) {
const key = [
'star-wars-movies',
id // Re-run `fetchMovie()` if `id` changes
]
const movie = useAsync(key, () => fetchMovie(id))
return (
<ul>
<li>
Title: {movie.title}
</li>
<li>
Release Date: {movie.release_date}
</li>
</ul>
)
}
See useAsync()
(Library Authors) for more information.
react-streaming
enables you to suspend React rendering and await something to happen. (Usually data fetching.)
The novelty here is that it's isomorphic:
You have the choice between three methods:
useAsync()
: High-level and easy.injectToStream()
: Low-level and highly flexible (useAsync()
is based on it). Easy & recommended for injecting script and style tags. Complex for data fetching (if possible, use useAsync()
instead).useAsync()
(Library Authors)This section is a low-level description of
useAsync()
. For a high-level description, seeuseAsync()
instead.
import { useAsync } from 'react-streaming'
function SomeComponent() {
const someAsyncFunc = async function () {
const value = 'someData'
return value
}
const key = ['some', 'invalidating', 'values']
// `useAsync()` suspends rendering until the promise returned by `someAsyncFunc()` resolves.
const value = useAsync(key, someAsyncFunc)
assert(value === 'someData')
}
When <SomeComponent>
is rendered on the server-side (SSR), it injects the
resolved value into the stream and the client-side picks up the injected value. This means that the
client-side doesn't call someAsyncFunc()
: instead, the client-side re-uses the value resolved on
the server-side.
If you want someAsyncFunc()
to be re-run, then change key
. The someAsyncFunc()
is only re-run if when the component is un-mounted and re-mounted, or if key
changes. For example, changing the state of your component (e.g. with useState()
) will not re-run someAsyncFunc()
if you provide the same key
.
Usually the key is set to ['name-of-the-function', ...functionArguments]
.
You can think of
key
to serve a similar purpose to React Queries's key, and to thedeps
argument of React'suseEffect(fn, deps)
.
injectToStream()
injectToStream(chunk: string | Buffer | unknown, options?: { flush?: boolean })
enables you to inject chunks to the current stream.
There are two ways to access injectToStream()
:
renderToStream()
:
import { renderToStream } from 'react-streaming/server'
const { injectToStream } = await renderToStream(<Page />)
useStream()
:
import { useStream } from 'react-streaming'
function SomeComponent() {
const stream = useStream()
if (stream === null) {
// No stream available. This is the case:
// - On the client-side.
// - When `option.disable === true`.
// - When react-streaming is not installed.
}
const { injectToStream } = stream
}
Usage examples:
// Inject JavaScript (e.g. for progressive hydration)
injectToStream('<script type="module" src="/main.js"></script>', { flush: true })
// Inject CSS (e.g. for CSS-in-JS)
injectToStream('<styles>.some-component { color: blue }</styles>', { flush: true })
// Pass data to client
injectToStream(`<script type="application/json">${JSON.stringify(someData)}</script>`)
For a full example of using injectToStream()
, have a look at useAsync()
's implementation.
If setting options.flush
to true
, then the stream will be flushed after chunk
has been written to the stream. This is only applicable for Node.js streams and only if you are using a compression library that makes a flush()
method available. For example, compression
adds a res.flush()
method. The option is ignored if there isn't a flush()
method available.
FAQs
React 18 Streaming. Full-fledged & Easy.
The npm package react-streaming receives a total of 5,033 weekly downloads. As such, react-streaming popularity was classified as popular.
We found that react-streaming demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.
Security News
NVD’s backlog surpasses 20,000 CVEs as analysis slows and NIST announces new system updates to address ongoing delays.
Security News
Research
A malicious npm package disguised as a WhatsApp client is exploiting authentication flows with a remote kill switch to exfiltrate data and destroy files.