diff --git a/react-instantsearch-hooks/remix/.eslintrc b/react-instantsearch-hooks/remix/.eslintrc
new file mode 100644
index 0000000000..63c8720ab8
--- /dev/null
+++ b/react-instantsearch-hooks/remix/.eslintrc
@@ -0,0 +1,7 @@
+{
+ "extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
+ "rules": {
+ "@typescript-eslint/naming-convention": "off",
+ "spaced-comment": ["error", "always", { "markers": ["/"] }]
+ }
+}
diff --git a/react-instantsearch-hooks/remix/.gitignore b/react-instantsearch-hooks/remix/.gitignore
new file mode 100644
index 0000000000..5f2cb537b5
--- /dev/null
+++ b/react-instantsearch-hooks/remix/.gitignore
@@ -0,0 +1,8 @@
+node_modules
+
+/.cache
+/build
+/public/build
+.env
+
+/app/tailwind.css
diff --git a/react-instantsearch-hooks/remix/README.md b/react-instantsearch-hooks/remix/README.md
new file mode 100644
index 0000000000..9659e785e0
--- /dev/null
+++ b/react-instantsearch-hooks/remix/README.md
@@ -0,0 +1,53 @@
+# Welcome to Remix!
+
+- [Remix Docs](https://remix.run/docs)
+
+## Development
+
+From your terminal:
+
+```sh
+npm run dev
+```
+
+This starts your app in development mode, rebuilding assets on file changes.
+
+## Deployment
+
+First, build your app for production:
+
+```sh
+npm run build
+```
+
+Then run the app in production mode:
+
+```sh
+npm start
+```
+
+Now you'll need to pick a host to deploy it to.
+
+### DIY
+
+If you're familiar with deploying node applications, the built-in Remix app server is production-ready.
+
+Make sure to deploy the output of `remix build`
+
+- `build/`
+- `public/build/`
+
+### Using a Template
+
+When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server.
+
+```sh
+cd ..
+# create a new project, and pick a pre-configured host
+npx create-remix@latest
+cd my-new-remix-app
+# remove the new project's app (not the old one!)
+rm -rf app
+# copy your app over
+cp -R ../my-old-remix-app/app app
+```
diff --git a/react-instantsearch-hooks/remix/app/entry.client.tsx b/react-instantsearch-hooks/remix/app/entry.client.tsx
new file mode 100644
index 0000000000..e313b977e5
--- /dev/null
+++ b/react-instantsearch-hooks/remix/app/entry.client.tsx
@@ -0,0 +1,4 @@
+import { RemixBrowser } from '@remix-run/react';
+import { hydrateRoot } from 'react-dom/client';
+
+hydrateRoot(document, );
diff --git a/react-instantsearch-hooks/remix/app/entry.server.tsx b/react-instantsearch-hooks/remix/app/entry.server.tsx
new file mode 100644
index 0000000000..9e20196f54
--- /dev/null
+++ b/react-instantsearch-hooks/remix/app/entry.server.tsx
@@ -0,0 +1,21 @@
+import type { EntryContext } from '@remix-run/node';
+import { RemixServer } from '@remix-run/react';
+import { renderToString } from 'react-dom/server';
+
+export default function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext
+) {
+ const markup = renderToString(
+
+ );
+
+ responseHeaders.set('Content-Type', 'text/html');
+
+ return new Response(`${markup}`, {
+ status: responseStatusCode,
+ headers: responseHeaders,
+ });
+}
diff --git a/react-instantsearch-hooks/remix/app/root.tsx b/react-instantsearch-hooks/remix/app/root.tsx
new file mode 100644
index 0000000000..f7c19e7e52
--- /dev/null
+++ b/react-instantsearch-hooks/remix/app/root.tsx
@@ -0,0 +1,32 @@
+import type { MetaFunction } from '@remix-run/node';
+import {
+ Links,
+ LiveReload,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from '@remix-run/react';
+
+export const meta: MetaFunction = () => ({
+ charset: 'utf-8',
+ title: 'React InstantSearch Hooks - Remix',
+ viewport: 'width=device-width,initial-scale=1',
+});
+
+export default function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/react-instantsearch-hooks/remix/app/routes/index.tsx b/react-instantsearch-hooks/remix/app/routes/index.tsx
new file mode 100644
index 0000000000..dff17af99e
--- /dev/null
+++ b/react-instantsearch-hooks/remix/app/routes/index.tsx
@@ -0,0 +1,122 @@
+import algoliasearch from 'algoliasearch/lite';
+import type { InstantSearchServerState } from 'react-instantsearch-hooks-web';
+import {
+ DynamicWidgets,
+ Hits,
+ InstantSearch,
+ InstantSearchSSRProvider,
+ Pagination,
+ RefinementList,
+ SearchBox,
+ useInstantSearch,
+} from 'react-instantsearch-hooks-web';
+import { getServerState } from 'react-instantsearch-hooks-server';
+import { history } from 'instantsearch.js/cjs/lib/routers/index.js';
+import instantSearchStyles from 'instantsearch.css/themes/satellite-min.css';
+
+import type { LinksFunction, LoaderFunction } from '@remix-run/node';
+import { json } from '@remix-run/node';
+import { useLoaderData } from '@remix-run/react';
+
+import { Hit } from '../../components/Hit';
+import { Panel } from '../../components/Panel';
+import { ScrollTo } from '../../components/ScrollTo';
+import { NoResultsBoundary } from '../../components/NoResultsBoundary';
+import { SearchErrorToast } from '../../components/SearchErrorToast';
+
+import tailwindStyles from '../tailwind.css';
+
+const searchClient = algoliasearch(
+ 'latency',
+ '6be0576ff61c053d5f9a3225e2a90f76'
+);
+
+export const links: LinksFunction = () => [
+ { rel: 'stylesheet', href: instantSearchStyles },
+ { rel: 'stylesheet', href: tailwindStyles },
+];
+
+export const loader: LoaderFunction = async ({ request }) => {
+ const serverUrl = request.url;
+ const serverState = await getServerState();
+
+ return json({
+ serverState,
+ serverUrl,
+ });
+};
+
+type SearchProps = {
+ serverState?: InstantSearchServerState;
+ serverUrl?: string;
+};
+
+function Search({ serverState, serverUrl }: SearchProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function FallbackComponent({ attribute }: { attribute: string }) {
+ return (
+
+
+
+ );
+}
+
+function NoResults() {
+ const { indexUiState } = useInstantSearch();
+
+ return (
+
+
+ No results for {indexUiState.query}
.
+
+
+ );
+}
+
+export default function HomePage() {
+ const { serverState, serverUrl } = useLoaderData();
+
+ return ;
+}
diff --git a/react-instantsearch-hooks/remix/components/Hit.tsx b/react-instantsearch-hooks/remix/components/Hit.tsx
new file mode 100644
index 0000000000..07741f6af9
--- /dev/null
+++ b/react-instantsearch-hooks/remix/components/Hit.tsx
@@ -0,0 +1,31 @@
+import type { Hit as AlgoliaHit } from 'instantsearch.js';
+import { Highlight } from 'react-instantsearch-hooks-web';
+
+type HitProps = {
+ hit: AlgoliaHit<{
+ name: string;
+ price: number;
+ image: string;
+ brand: string;
+ }>;
+};
+
+export function Hit({ hit }: HitProps) {
+ return (
+
+
+
+
+
+
+
+
+
{hit.brand}
+
${hit.price}
+
+ );
+}
diff --git a/react-instantsearch-hooks/remix/components/NoResultsBoundary.tsx b/react-instantsearch-hooks/remix/components/NoResultsBoundary.tsx
new file mode 100644
index 0000000000..cf7f6090c1
--- /dev/null
+++ b/react-instantsearch-hooks/remix/components/NoResultsBoundary.tsx
@@ -0,0 +1,27 @@
+import type { ReactNode } from 'react';
+import { useInstantSearch } from 'react-instantsearch-hooks-web';
+
+type NoResultsBoundaryProps = {
+ children: ReactNode;
+ fallback: ReactNode;
+};
+
+export function NoResultsBoundary({
+ children,
+ fallback,
+}: NoResultsBoundaryProps) {
+ const { results } = useInstantSearch();
+
+ // The `__isArtificial` flag makes sure to not display the No Results message
+ // when no hits have been returned yet.
+ if (!results.__isArtificial && results.nbHits === 0) {
+ return (
+ <>
+ {fallback}
+ {children}
+ >
+ );
+ }
+
+ return <>{children}>;
+}
diff --git a/react-instantsearch-hooks/remix/components/Panel.tsx b/react-instantsearch-hooks/remix/components/Panel.tsx
new file mode 100644
index 0000000000..1fdd24737a
--- /dev/null
+++ b/react-instantsearch-hooks/remix/components/Panel.tsx
@@ -0,0 +1,17 @@
+export function Panel({
+ children,
+ header,
+ footer,
+}: {
+ children: React.ReactNode;
+ header?: React.ReactNode;
+ footer?: React.ReactNode;
+}) {
+ return (
+
+ {header &&
{header}
}
+
{children}
+ {footer &&
{footer}
}
+
+ );
+}
diff --git a/react-instantsearch-hooks/remix/components/ScrollTo.tsx b/react-instantsearch-hooks/remix/components/ScrollTo.tsx
new file mode 100644
index 0000000000..2916c42426
--- /dev/null
+++ b/react-instantsearch-hooks/remix/components/ScrollTo.tsx
@@ -0,0 +1,37 @@
+import type { ComponentProps, ReactNode } from 'react';
+import React, { useEffect, useRef } from 'react';
+import { useInstantSearch } from 'react-instantsearch-hooks-web';
+
+type ScrollToProps = ComponentProps<'div'> & {
+ children: ReactNode;
+};
+
+export function ScrollTo({ children, ...props }: ScrollToProps) {
+ const { use } = useInstantSearch();
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ return use(() => {
+ return {
+ onStateChange() {
+ const isFiltering = document.body.classList.contains('filtering');
+ const isTyping =
+ document.activeElement?.tagName === 'INPUT' &&
+ document.activeElement?.getAttribute('type') === 'search';
+
+ if (isFiltering || isTyping) {
+ return;
+ }
+
+ containerRef.current!.scrollIntoView();
+ },
+ };
+ });
+ }, [use]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/react-instantsearch-hooks/remix/components/SearchErrorToast.tsx b/react-instantsearch-hooks/remix/components/SearchErrorToast.tsx
new file mode 100644
index 0000000000..7b7a9cd5a6
--- /dev/null
+++ b/react-instantsearch-hooks/remix/components/SearchErrorToast.tsx
@@ -0,0 +1,58 @@
+import React, { useEffect, useState } from 'react';
+import * as Toast from '@radix-ui/react-toast';
+import { useInstantSearch } from 'react-instantsearch-hooks-web';
+
+export function SearchErrorToast() {
+ const { use } = useInstantSearch();
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ return use(({ instantSearchInstance }) => {
+ function handleError(searchError: Error) {
+ setError(searchError);
+ }
+
+ return {
+ subscribe() {
+ instantSearchInstance.addListener('error', handleError);
+ },
+ unsubscribe() {
+ instantSearchInstance.removeListener('error', handleError);
+ },
+ };
+ });
+ }, [use]);
+
+ if (!error) {
+ return null;
+ }
+
+ return (
+
+ {
+ if (!isOpen) {
+ setError(null);
+ }
+ }}
+ >
+
+
+
+
+
+ {error.name}
+
+
+ {error.message}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/react-instantsearch-hooks/remix/package.json b/react-instantsearch-hooks/remix/package.json
new file mode 100644
index 0000000000..b2e2f0e2b1
--- /dev/null
+++ b/react-instantsearch-hooks/remix/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "hooks-remix-example",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "build": "run-s \"build:*\"",
+ "build:css": "yarn run generate:css --minify",
+ "build:remix": "remix build",
+ "dev": "run-p \"dev:*\"",
+ "dev:css": "yarn run generate:css --watch",
+ "dev:remix": "remix dev",
+ "generate:css": "npx tailwindcss -o ./app/tailwind.css",
+ "start": "remix-serve build"
+ },
+ "dependencies": {
+ "@radix-ui/react-toast": "0.1.1",
+ "@remix-run/node": "1.6.5",
+ "@remix-run/react": "1.6.5",
+ "@remix-run/serve": "1.6.5",
+ "algoliasearch": "4.11.0",
+ "instantsearch.css": "7.4.5",
+ "react": "18.1.0",
+ "react-dom": "18.1.0",
+ "react-instantsearch-hooks-server": "6.30.2",
+ "react-instantsearch-hooks-web": "6.30.2"
+ },
+ "devDependencies": {
+ "@remix-run/dev": "1.6.5",
+ "@remix-run/eslint-config": "1.6.5",
+ "@types/react": "18.0.17",
+ "@types/react-dom": "18.0.6",
+ "npm-run-all": "4.1.5",
+ "tailwindcss": "3.1.6",
+ "typescript": "4.7.4"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+}
diff --git a/react-instantsearch-hooks/remix/public/favicon.ico b/react-instantsearch-hooks/remix/public/favicon.ico
new file mode 100644
index 0000000000..8830cf6821
Binary files /dev/null and b/react-instantsearch-hooks/remix/public/favicon.ico differ
diff --git a/react-instantsearch-hooks/remix/remix.config.js b/react-instantsearch-hooks/remix/remix.config.js
new file mode 100644
index 0000000000..2eee2ab3f5
--- /dev/null
+++ b/react-instantsearch-hooks/remix/remix.config.js
@@ -0,0 +1,8 @@
+/** @type {import('@remix-run/dev').AppConfig} */
+module.exports = {
+ ignoredRouteFiles: ['**/.*'],
+ // appDirectory: "app",
+ // assetsBuildDirectory: "public/build",
+ // serverBuildPath: "build/index.js",
+ // publicPath: "/build/",
+};
diff --git a/react-instantsearch-hooks/remix/remix.env.d.ts b/react-instantsearch-hooks/remix/remix.env.d.ts
new file mode 100644
index 0000000000..72e2affe31
--- /dev/null
+++ b/react-instantsearch-hooks/remix/remix.env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/react-instantsearch-hooks/remix/sandbox.config.json b/react-instantsearch-hooks/remix/sandbox.config.json
new file mode 100644
index 0000000000..be139c9097
--- /dev/null
+++ b/react-instantsearch-hooks/remix/sandbox.config.json
@@ -0,0 +1,4 @@
+{
+ "template": "node",
+ "container": { "port": 3000, "startScript": "dev" }
+}
\ No newline at end of file
diff --git a/react-instantsearch-hooks/remix/tailwind.config.js b/react-instantsearch-hooks/remix/tailwind.config.js
new file mode 100644
index 0000000000..f7b2a44a53
--- /dev/null
+++ b/react-instantsearch-hooks/remix/tailwind.config.js
@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./app/**/*.{ts,tsx,jsx,js}', './components/**/*.{ts,tsx,jsx,js}'],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+};
diff --git a/react-instantsearch-hooks/remix/tsconfig.json b/react-instantsearch-hooks/remix/tsconfig.json
new file mode 100644
index 0000000000..20f8a386a6
--- /dev/null
+++ b/react-instantsearch-hooks/remix/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2019"],
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "target": "ES2019",
+ "strict": true,
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+
+ // Remix takes care of building everything in `remix build`.
+ "noEmit": true
+ }
+}