Skip to content

Commit

Permalink
fix(react): fix compatibility issues with React 18 (#969)
Browse files Browse the repository at this point in the history
  • Loading branch information
sarahdayan authored May 10, 2022
1 parent 644cc8e commit fb46298
Show file tree
Hide file tree
Showing 16 changed files with 376 additions and 10 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ examples/twitter-compose-with-typeahead
examples/slack-with-emojis-and-commands
examples/react-instantsearch-hooks
examples/vue-instantsearch
examples/react-18
1 change: 1 addition & 0 deletions examples/react-18/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SKIP_PREFLIGHT_CHECK=true
23 changes: 23 additions & 0 deletions examples/react-18/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
32 changes: 32 additions & 0 deletions examples/react-18/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Autocomplete with React 18 example

This example shows how to integrate Autocomplete with [React 18](https://reactjs.org/blog/2022/03/29/react-v18.html).

## Demo

[Access the demo](https://codesandbox.io/s/github/algolia/autocomplete/tree/next/examples/react-18)

## How to run this example locally

### 1. Clone this repository

```sh
git clone [email protected]:algolia/autocomplete.git
```

### 2. Install the dependencies and run the server

```sh
yarn
yarn workspace @algolia/autocomplete-example-react-18 start
```

Alternatively, you may use npm:

```sh
cd examples/react-18
npm install
npm start
```

Open <http://localhost:3000> to see your app.
42 changes: 42 additions & 0 deletions examples/react-18/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@algolia/autocomplete-example-react-18",
"description": "Autocomplete example with React 18",
"version": "1.6.2",
"private": true,
"license": "MIT",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
"dependencies": {
"@algolia/autocomplete-js": "1.6.2",
"@algolia/autocomplete-theme-classic": "1.6.2",
"algoliasearch": "4.9.1",
"react": "^18.1.0",
"react-dom": "^18.1.0"
},
"devDependencies": {
"@algolia/client-search": "4.9.1",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"react-scripts": "4.0.3",
"typescript": "^4.4.2"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Binary file added examples/react-18/public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions examples/react-18/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>React 18 | Autocomplete</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
113 changes: 113 additions & 0 deletions examples/react-18/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useEffect, useRef, createElement, Fragment } from 'react';
import { createRoot } from 'react-dom/client';
import { autocomplete, getAlgoliaResults } from '@algolia/autocomplete-js';
import algoliasearch from 'algoliasearch/lite';

import type { AutocompleteComponents } from '@algolia/autocomplete-js';
import type { Hit } from '@algolia/client-search';
import type { Root } from 'react-dom/client';

import '@algolia/autocomplete-theme-classic';

const appId = 'latency';
const apiKey = '6be0576ff61c053d5f9a3225e2a90f76';
const searchClient = algoliasearch(appId, apiKey);

type ProductHit = Hit<{
brand: string;
categories: string[];
description: string;
image: string;
name: string;
price: number;
rating: number;
type: string;
url: string;
}>;

export default function App() {
const containerRef = useRef<HTMLDivElement | null>(null);
const panelRootRef = useRef<Root | null>(null);
const rootRef = useRef<HTMLElement | null>(null);

useEffect(() => {
if (!containerRef.current) {
return undefined;
}

const search = autocomplete<ProductHit>({
container: containerRef.current,
placeholder: 'Search',
getSources({ query }) {
return [
{
sourceId: 'products',
getItems() {
return getAlgoliaResults<ProductHit>({
searchClient,
queries: [
{
indexName: 'instant_search',
query,
},
],
});
},
templates: {
item({ item, components }) {
return <ProductItem hit={item} components={components} />;
},
noResults() {
return 'No products matching.';
},
},
},
];
},
renderer: { createElement, Fragment, render: () => {} },
render({ children }, root) {
if (!panelRootRef.current || rootRef.current !== root) {
rootRef.current = root;

panelRootRef.current?.unmount();
panelRootRef.current = createRoot(root);
}

panelRootRef.current.render(children);
},
});

return () => {
search.destroy();
};
}, []);

return <div ref={containerRef} />;
}

type ProductItemProps = {
hit: ProductHit;
components: AutocompleteComponents;
};

function ProductItem({ hit, components }: ProductItemProps) {
return (
<div className="aa-ItemWrapper">
<div className="aa-ItemContent">
<div className="aa-ItemIcon aa-ItemIcon--picture aa-ItemIcon--alignTop">
<img src={hit.image} alt={hit.name} width="40" height="40" />
</div>

<div className="aa-ItemContentBody">
<div className="aa-ItemContentTitle">
<components.Highlight hit={hit} attribute="name" />
</div>
<div className="aa-ItemContentDescription">
By <strong>{hit.brand}</strong> in{' '}
<strong>{hit.categories[0]}</strong>
</div>
</div>
</div>
</div>
);
}
14 changes: 14 additions & 0 deletions examples/react-18/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

import './styles.css';

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
1 change: 1 addition & 0 deletions examples/react-18/src/react-app-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="react-scripts" />
20 changes: 20 additions & 0 deletions examples/react-18/src/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
* {
box-sizing: border-box;
}

body {
background-color: rgb(244, 244, 249);
color: rgb(65, 65, 65);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 1rem;
}

.container {
margin: 0 auto;
max-width: 640px;
width: 100%;
}
26 changes: 26 additions & 0 deletions examples/react-18/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}
40 changes: 40 additions & 0 deletions packages/autocomplete-js/src/__tests__/renderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ describe('renderer', () => {
'\nTo get rid of this warning, do any of the following depending on your use case.' +
"\n- If you are using the `render` option only to override Autocomplete's default `render` function, pass the `render` function into `renderer` and remove the `render` option." +
'\n- If you are using the `render` option to customize the layout, pass your `render` function into `renderer` and use it from the provided parameters of the `render` option.' +
'\n- If you are using the `render` option to work with React 18, pass an empty `render` function into `renderer`.' +
'\nSee https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-js/autocomplete/#param-render'
);

Expand Down Expand Up @@ -475,6 +476,45 @@ describe('renderer', () => {
}).not.toWarnDev();
});

test('does not warn at all when passing an empty `renderer.render` function', () => {
const container = document.createElement('div');
const panelContainer = document.createElement('div');
const CustomFragment = (props: any) => props.children;
const mockCreateElement = jest.fn().mockImplementation(preactCreateElement);

document.body.appendChild(panelContainer);

expect(() => {
autocomplete<{ label: string }>({
container,
panelContainer,
initialState: {
isOpen: true,
},
getSources() {
return [
{
sourceId: 'testSource',
getItems() {
return [{ label: '1' }];
},
templates: {
item({ item }) {
return item.label;
},
},
},
];
},
renderer: {
createElement: mockCreateElement,
Fragment: CustomFragment,
render: () => {},
},
});
}).not.toWarnDev();
});

test('does not warn at all when not passing a custom renderer', () => {
const container = document.createElement('div');
const panelContainer = document.createElement('div');
Expand Down
1 change: 1 addition & 0 deletions packages/autocomplete-js/src/getDefaultOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export function getDefaultOptions<TItem extends BaseItem>(
`\nTo get rid of this warning, do any of the following depending on your use case.` +
"\n- If you are using the `render` option only to override Autocomplete's default `render` function, pass the `render` function into `renderer` and remove the `render` option." +
'\n- If you are using the `render` option to customize the layout, pass your `render` function into `renderer` and use it from the provided parameters of the `render` option.' +
'\n- If you are using the `render` option to work with React 18, pass an empty `render` function into `renderer`.' +
'\nSee https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-js/autocomplete/#param-render'
);

Expand Down
15 changes: 5 additions & 10 deletions packages/autocomplete-js/src/types/AutocompleteRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,14 @@ export type Pragma = (
) => JSX.Element;
export type PragmaFrag = any;

type ComponentChild =
| VNode<any>
| object
| string
| number
| boolean
| null
| undefined;
type ComponentChild = VNode<any> | string | number | boolean | null | undefined;

type ComponentChildren = ComponentChild[] | ComponentChild;

export type VNode<TProps = any> = {
export type VNode<TProps = {}> = {
type: any;
props: TProps & { children: ComponentChildren; key?: any };
key: string | number | any;
props: TProps & { children: ComponentChildren };
};

export type Render = (
Expand Down
Loading

0 comments on commit fb46298

Please sign in to comment.