- Jun 2023
-
sergiodxa.com sergiodxa.com
-
www.youtube.com www.youtube.comYouTube1
-
-
www.enterspeed.com www.enterspeed.com
-
Tags
Annotators
URL
-
- May 2023
-
remix.run remix.run
Tags
Annotators
URL
-
-
remix-pwa-docs.vercel.app remix-pwa-docs.vercel.app
Tags
Annotators
URL
-
- Apr 2023
-
Tags
Annotators
URL
-
-
twitter.com twitter.com
Tags
Annotators
URL
-
-
sergiodxa.com sergiodxa.com
Tags
Annotators
URL
-
-
codesandbox.io codesandbox.io
-
-
codesandbox.io codesandbox.io
-
-
Tags
Annotators
URL
-
-
stackoverflow.com stackoverflow.com
-
dev.to dev.to
-
codegino.com codegino.com
Tags
Annotators
URL
-
-
-
The paths must be relative to the project root. You should use ./ui/**/* so it also watches subfolders of ui.
js module.exports = { watchPaths: ['./ui/**/*'] }
Tags
Annotators
URL
-
-
vite-remix-router.vercel.app vite-remix-router.vercel.app
Tags
Annotators
URL
-
-
codesandbox.io codesandbox.io
-
-
daily-dev-tips.com daily-dev-tips.com
-
daily-dev-tips.com daily-dev-tips.com
- Mar 2023
-
www.drewis.cool www.drewis.cool
-
www.remixfast.com www.remixfast.com
Tags
Annotators
URL
-
-
spin.atomicobject.com spin.atomicobject.com
Tags
Annotators
URL
-
-
sergiodxa.com sergiodxa.com
Tags
Annotators
URL
-
-
www.npmjs.com www.npmjs.com
-
www.youtube.com www.youtube.com
-
-
www.mattstobbs.com www.mattstobbs.com
Tags
Annotators
URL
-
-
www.mattstobbs.com www.mattstobbs.com
Tags
Annotators
URL
-
-
remix-scoped-params-production.up.railway.app remix-scoped-params-production.up.railway.app
-
remix.run remix.run
-
-
remix-routing-demo.netlify.app remix-routing-demo.netlify.app
Tags
Annotators
URL
-
-
gist.github.com gist.github.com
-
www.youtube.com www.youtube.com
-
-
remix.run remix.run
Tags
Annotators
URL
-
-
www.infoxicator.com www.infoxicator.com
-
-
reactrouter.com reactrouter.com
-
-
```js // Set up some pub/sub on the server
import { EventEmitter } from "events"; export let emitter = new EventEmitter();
// Set up an event stream with cleanup and queues // and stuff that subscribes to it and streams the // events when new stuff comes through:
import { emitter } from "../some-emitter.server";
type InitFunction = (send: SendFunction) => CleanupFunction; type SendFunction = (event: string, data: string) => void; type CleanupFunction = () => void;
export function eventStream(request: Request, init: InitFunction) { let stream = new ReadableStream({ start(controller) { let encoder = new TextEncoder(); let send = (event: string, data: string) => { controller.enqueue(encoder.encode(
event: ${event}\n
)); controller.enqueue(encoder.encode(data: ${data}\n\n
)); }; let cleanup = init(send);let closed = false; let close = () => { if (closed) return; cleanup(); closed = true; request.signal.removeEventListener("abort", close); controller.close(); }; request.signal.addEventListener("abort", close); if (request.signal.aborted) { close(); return; } },
});
return new Response(stream, { headers: { "Content-Type": "text/event-stream" }, }); }
// Return the event stream from a loader in // a resource route:
import { eventStream } from "./event-stream";
export let loader: LoaderFunction = ({ request }) => { return eventStream(request, send => { emitter.addListener("messageReceived", handleChatMessage);
function handleChatMessage(chatMessage: string) { send("message", chatMessage); } return () => { emitter.removeListener("messageReceived", handleChatMessage); };
}); };
// Push into the event emitter in actions:
import { emitter } from "./some-emitter.server";
export let action: ActionFunction = async ({ request }) => { let formData = await request.formData(); emitter.emit("messageReceived", formData.get("something"); return { ok: true }; };
// And finally, set up an EventSource in the browser
function useEventSource(href: string) { let [data, setData] = useState("");
useEffect(() => { let eventSource = new EventSource(href); eventSource.addEventListener("message", handler);
function handler(event: MessageEvent) { setData(event.data || "unknown"); } return () => { eventSource.removeEventListener("message", handler); };
}, []);
return data; } ```
Tags
Annotators
URL
-
-
sergiodxa.com sergiodxa.com
-
Put that TS code in a file your app imports, for example, in remix.env.d.ts, and now the type of name will be the expected one.
ts declare module "@remix-run/server-runtime" { export interface AppLoadContext { name: string; } }
-
-
codesandbox.io codesandbox.io
-
-
github.com github.com
-
www.mattstobbs.com www.mattstobbs.com
Tags
Annotators
URL
-
-
www.infoxicator.com www.infoxicator.com
-
www.infoxicator.com www.infoxicator.com
-
```js
export const loader = async () => {
// fire them all at once<br /> const critical1Promise = fetch('/test?text=critical1&delay=250').then(res => res.json()); const critical2Promise = fetch('/test?text=critical2&delay=500').then(res => res.json()); const lazyResolvedPromise = fetch('/test?text=lazyResolved&delay=100').then(res => res.json()); const lazy1Promise = fetch('/test?text=lazy1&delay=500').then(res => res.json()); const lazy2Promise = fetch('/test?text=lazy2&delay=1500').then(res => res.json()); const lazy3Promise = fetch('/test?text=lazy3&delay=2500').then(res => res.json()); const lazyErrorPromise = fetch('/test?text=lazy3&delay=3000').then(res => { throw Error('Oh noo!') });
// await for the response return defer({ critical1: await critical1Promise, critical2: await critical2Promise, lazyResolved: lazyResolvedPromise, lazy1: lazy1Promise, lazy2: lazy2Promise, lazy3: lazy3Promise, lazyError: lazyErrorPromise }) } ```
-
-
hydrogen.shopify.dev hydrogen.shopify.devRoadmap1
-
```js import {defer} from "@shopify/remix-oxygen";
export async function loader({ params: {handle}, context: {storefront} }) { const {product} = storefront.query({ query:
#graphql query Product( $country: CountryCode, $language: LanguageCode, $handle: String! ) @inContext(country: $country, language: $language) product(handle: $handle) { id title }
, variables: {handle}, cache: storefront.CacheLong() }); const {productRecommendations} = storefront.query({ query:#graphql query ProductRecommendations( $country: CountryCode, $language: LanguageCode, $handle: String! ) @inContext(country: $country, language: $language) productRecommendations(handle: $handle) { id title } }
, variables: {handle} }); if (!product) { throw new Response('Not Found', { status: 404, }); } return defer({ product: await product, productRecommendations, }); } ```
Tags
Annotators
URL
-
-
www.npmjs.com www.npmjs.com
Tags
Annotators
URL
-
-
github.com github.com
-
www.jacobparis.com www.jacobparis.com
-
We present deferred data by using React Suspense to conditionally show the content when it's ready. Suspense provides a fallback element to show when the data is not yet ready. Normally a loading spinner would go here, but we can use that to show our streamed progress instead.
js export default function Index() { const data = useLoaderData() const params = useParams() const stream = useEventSource( `/items/${params.hash}/progress`, { event: "progress", }, ) return ( <div> <Suspense fallback={<span> {stream}% </span>}> <Await resolve={data.promise} errorElement={<p>Error loading img!</p>} > {(promise) => <img alt="" src={promise.img} />} </Await> </Suspense> </div> ) }
-
On the client, while we're waiting for our deferred promise to resolve, we can consume that stream to know how far along our process is.
js const stream = useEventSource( `/items/${params.hash}/progress`, { event: "progress", }, )
-
In Remix, we can use a resource route to make this endpoint, and our loader will return a stream that constant checks our JSON file for its progress.
js export async function loader({ request, params, }: LoaderArgs) { const hash = params.hash return eventStream(request.signal, function setup(send) { const interval = setInterval(() => { const file = fs.readFileSync( path.join("public", "items", `${hash}.json`), ) if (file.toString()) { const data = JSON.parse(file.toString()) const progress = data.progress send({ event: "progress", data: String(progress) }) if (progress === 100) { clearInterval(interval) } } }, 200) return function clear(timer: number) { clearInterval(interval) clearInterval(timer) } }) }
-
server sent events work by having an endpoint that does not immediately close its connection, and which sends a content type of text/event-stream.
-
The loader defers a promise that will resolve only when the json's progress has hit 100.
js export async function loader({ params }: LoaderArgs) { if (!params.hash) return redirect("/") const pathname = path.join( "public", "items", `${params.hash}.json`, ) const file = fs.readFileSync(pathname) if (!file) return redirect("/") const item = JSON.parse(file.toString()) if (!item) return redirect("/") if (item.progress === 100) { return defer({ promise: item, }) } return defer({ promise: new Promise((resolve) => { const interval = setInterval(() => { const file = fs.readFileSync(pathname) if (!file) return const item = JSON.parse(file.toString()) if (!item) return if (item.progress === 100) { clearInterval(interval) resolve(item) } return }) }), }) }
-
Defer is a feature of Remix that allows you to return an unresolved Promise from a loader. The page will server-side render without waiting for the promise to resolve, and then when it finally does, the client will re-render with the new data.
-
-
gist.github.com gist.github.com
-
-
What could be happening is dates being parsed with a different timezone during SSR and CSR for example.
Tags
Annotators
URL
-
-
remix.run remix.run
-
-
www.youtube.com www.youtube.com
-
-
remix.run remix.run
Tags
Annotators
URL
-
-
Tags
Annotators
URL
-
-
Tags
Annotators
URL
-
-
-
Remix uses the ?index parameter to indicate when a URL refers to the index route instead of the layout route
Tags
Annotators
URL
-
-
Tags
Annotators
URL
-
-
donavon.com donavon.com
Tags
Annotators
URL
-
-
sergiodxa.com sergiodxa.com
-
Send the 304 Not Modified response
```js import etag from "etag"; import { renderToString } from "react-dom/server"; import type { EntryContext, HandleDataRequestFunction } from "remix"; import { RemixServer } from "remix";
export default function handleRequest( request: Request, status: number, headers: Headers, remixContext: EntryContext ) { let markup = renderToString( <RemixServer context={remixContext} url={request.url} /> );
headers.set("Content-Type", "text/html"); headers.set("ETag", etag(markup));
// check if the
If-None-Match
header matches the ETag if (request.headers.get("If-None-Match") === headers.get("ETag")) { // and send an empty Response with status 304 and the headers. return new Response("", { status: 304, headers }); }return new Response("<!DOCTYPE html>" + markup, { status, headers }); }
export let handleDataRequest: HandleDataRequestFunction = async ( response: Response, { request } ) => { let body = await response.text();
if (request.method.toLowerCase() === "get") { response.headers.set("etag", etag(body)); // As with document requests, check the
If-None-Match
header // and compare it with the Etag, if they match, send the empty 304 Response if (request.headers.get("If-None-Match") === response.headers.get("ETag")) { return new Response("", { status: 304, headers: response.headers }); } }return response; }; ```
-
All Together
```js import etag from "etag"; import { renderToString } from "react-dom/server"; import type { EntryContext, HandleDataRequestFunction } from "remix"; import { RemixServer } from "remix";
export default function handleRequest( request: Request, status: number, headers: Headers, remixContext: EntryContext ) { let markup = renderToString( <RemixServer context={remixContext} url={request.url} /> );
headers.set("Content-Type", "text/html"); headers.set("ETag", etag(markup));
return new Response("<!DOCTYPE html>" + markup, { status, headers }); }
export let handleDataRequest: HandleDataRequestFunction = async ( response: Response ) => { let body = await response.text(); response.headers.set("etag", etag(body)); return response; }; ```
-
Using ETags for document requests
```js import etag from "etag"; import { renderToString } from "react-dom/server"; import type { EntryContext } from "remix"; import { RemixServer } from "remix";
export default function handleRequest( request: Request, status: number, headers: Headers, remixContext: EntryContext ) { let markup = renderToString( <RemixServer context={remixContext} url={request.url} /> );
headers.set("Content-Type", "text/html"); // add the Etag header using the markup as value headers.set("ETag", etag(markup));
return new Response("<!DOCTYPE html>" + markup, { status, headers }); } ```
-
Using ETags for data requests
```js import etag from "etag"; import type { HandleDataRequestFunction } from "remix";
export let handleDataRequest: HandleDataRequestFunction = async ( response: Response, { request } ) => { let body = await response.text(); // parse the response body as text
// only add the ETag for GET requests if (request.method.toLowerCase() === "get") { response.headers.set("etag", etag(body)); // and use it to create the ETag }
return response; // return the response }; ```
-
-
-
www.npmjs.com www.npmjs.com
-
```js import { renderToString } from "react-dom/server"; import { RemixServer } from "remix"; import type { EntryContext } from "remix"; import { etag } from 'remix-etag';
export default function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext ) { const markup = renderToString( <RemixServer context={remixContext} url={request.url} /> );
responseHeaders.set("Content-Type", "text/html");
const response = new Response("<!DOCTYPE html>" + markup, { status: responseStatusCode, headers: responseHeaders, }); return etag({ request, response }); } ```
Tags
Annotators
URL
-
-
Tags
Annotators
URL
-
-
github.com github.com
-
```js import { renderToReadableStream } from 'react-dom/server'; import type { EntryContext } from '@remix-run/cloudflare'; import { RemixServer } from '@remix-run/react'; import { renderHeadToString } from 'remix-island'; import { Head } from './root';
const readableString = (value: string) => { const te = new TextEncoder(); return new ReadableStream({ start(controller) { controller.enqueue(te.encode(value)); controller.close(); }, }); };
export default async function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext, ) { const { readable, writable } = new TransformStream(); const head = readableString(
<!DOCTYPE html><html><head>${renderHeadToString({ request, remixContext, Head, })}</head><body><div id="root">
, ); const end = readableString(</div></body></html>
);const body = await renderToReadableStream( <RemixServer context={remixContext} url={request.url} />, );
Promise.resolve() .then(() => head.pipeTo(writable, { preventClose: true })) .then(() => body.pipeTo(writable, { preventClose: true })) .then(() => end.pipeTo(writable));
responseHeaders.set('Content-Type', 'text/html');
return new Response(readable, { status: responseStatusCode, headers: responseHeaders, }); } ```
Tags
Annotators
URL
-
-
developers.cloudflare.com developers.cloudflare.com
-
www.youtube.com www.youtube.com
Tags
Annotators
URL
-
-
tanstack.com tanstack.com
Tags
Annotators
URL
-
-
gist.github.com gist.github.com
-
You'll notice that for the app/routes/jokes/$jokeId.tsx route in addition to Cache-Control we've also set Vary header to Cookie. This is because we're returning something that's specific to the user who is logged in. So we want the cache to associated to that particular Cookie value and not shared with different users, so the browser and CDN will not deliver the cached value if the cookie is different from the cached response's cookie.
-
-
-
Tags
Annotators
URL
-
-
www.youtube.com www.youtube.com
-
-
-
```js import parseCacheControl from "parse-cache-control";
export function headers({ loaderHeaders, parentHeaders, }: { loaderHeaders: Headers; parentHeaders: Headers; }) { const loaderCache = parseCacheControl( loaderHeaders.get("Cache-Control") ); const parentCache = parseCacheControl( parentHeaders.get("Cache-Control") );
// take the most conservative between the parent and loader, otherwise // we'll be too aggressive for one of them. const maxAge = Math.min( loaderCache["max-age"], parentCache["max-age"] );
return { "Cache-Control":
max-age=${maxAge}
, }; } ```
```js import { renderToString } from "react-dom/server"; import { RemixServer } from "@remix-run/react"; import type { EntryContext } from "@remix-run/node"; // or cloudflare/deno
export default function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext ) { const markup = renderToString( <RemixServer context={remixContext} url={request.url} /> );
responseHeaders.set("Content-Type", "text/html"); responseHeaders.set("X-Powered-By", "Hugs");
return new Response("<!DOCTYPE html>" + markup, { status: responseStatusCode, headers: responseHeaders, }); } ```
Tags
Annotators
URL
-
- Feb 2023
-
github.com github.com
-
```js import type { EntryContext } from "@remix-run/cloudflare"; import { RemixServer } from "@remix-run/react"; import isbot from "isbot"; import { renderToReadableStream } from "react-dom/server";
const ABORT_DELAY = 5000;
const handleRequest = async ( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext ) => { let didError = false;
const stream = await renderToReadableStream( <RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />, { onError: (error: unknown) => { didError = true; console.error(error);
// You can also log crash/error report }, signal: AbortSignal.timeout(ABORT_DELAY), }
);
if (isbot(request.headers.get("user-agent"))) { await stream.allReady; }
responseHeaders.set("Content-Type", "text/html"); return new Response(stream, { headers: responseHeaders, status: didError ? 500 : responseStatusCode, }); };
export default handleRequest; ```
-
-
tom-sherman.com tom-sherman.com
-
www.youtube.com www.youtube.com
-
-
www.youtube.com www.youtube.com
-
-
medium.com medium.com
-
-
I resolved this by changing the imports from @remix-run/node to @remix-run/cloudflare
diff - import { json } from "@remix-run/node"; + import { json } from "@remix-run/cloudflare";
-
-
remix.run remix.run
Tags
Annotators
URL
-
-
remix.run remix.run
Tags
Annotators
URL
-
-
remix.run remix.run
Tags
Annotators
URL
-
- Dec 2022
-
blog.logrocket.com blog.logrocket.com
Tags
Annotators
URL
-
-
stackoverflow.com stackoverflow.com
-
I had been wrapping my components with an improper tag that is, NextJS is not comfortable having a p tag wrapping your divs, sections etc so it will yell "Hydration failed because the initial UI does not match what was rendered on the server". So I solved this problem by examining how my elements were wrapping each other.
-
-
dev.to dev.to
-
www.jacobparis.com www.jacobparis.com
Tags
Annotators
URL
-
-
developers.cloudflare.com developers.cloudflare.com
-
www.smashingmagazine.com www.smashingmagazine.com
-
codesandbox.io codesandbox.io
-
- Nov 2022
-
bejamas.io bejamas.io
Tags
Annotators
URL
-
-
gist.github.com gist.github.com
-
dev.to dev.to
Tags
Annotators
URL
-
-
codesandbox.io codesandbox.io
-
-
mediajams.dev mediajams.dev
-
-
news.ycombinator.com news.ycombinator.com
-
If we're talking "load more" a much simpler way to do it is to consider how you'd do it old school with no JS. Just return all pages according to the search param, so "?page=3" would return all three pages. More kB over the network, but far easier to implement. There's even a product reason to do it this way: when you load more you automatically update the comment counts/points of each entry, so maybe it's worth it.
-
Location state, unlike typical "global state", automatically persists across both refreshes and back/forward button clicks, but dies with the session, so scroll restoration will work as expected even for a refresh! Built in browser APIs tend to be a bit more resilient than storing stuff in application memory, they also keep your bundle smaller.
-
There are various ways to use it (declaratively, imperatively, etc.), but simplest way to explain is to imagine a "Load more" button that's really just a link like this:`<Link to="?page=2" state={{ data }} />`Your Remix loader would load page 2 from the url search param and your component would concat that onto `location.state.data` (the previous entries from the initial location). This renders the old data and the new data together.
-
-
nikzb.hashnode.dev nikzb.hashnode.dev
-
css-tricks.com css-tricks.com
-
-
www.remixfast.com www.remixfast.com
Tags
Annotators
URL
-
-
www.epicweb.dev www.epicweb.dev
Tags
Annotators
URL
-
-
kentcdodds.com kentcdodds.com
Tags
Annotators
URL
-
-
moduscreate.com moduscreate.com
-
egghead.io egghead.io
-
remix.run remix.run
Tags
Annotators
URL
-
-
Tags
Annotators
URL
-
- Oct 2022
-
twitter.com twitter.com
- Sep 2022
-
www.youtube.com www.youtube.comYouTube1
-
- Jul 2022
-
www.smashingmagazine.com www.smashingmagazine.com
- Jan 2022
-
remix.guide remix.guide
-