Skip to content

Commit

Permalink
Document React Native Web Support (#709)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcandillon authored Jul 22, 2022
1 parent 51dc617 commit f76a1c9
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 18 deletions.
4 changes: 4 additions & 0 deletions docs/docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ For error **_CMake 'X.X.X' was not found in SDK, PATH, or by cmake.dir property.
open _Tools > SDK Manager_, switch to the _SDK Tools_ tab.
Find `CMake` and click _Show Package Details_ and download compatiable version **'X.X.X'**, and apply to install.

## Web

To use this library in the browser, see [these instructions](/docs/getting-started/web).

## Playground

We have an example project you can play with [here](https://github.com/Shopify/react-native-skia/tree/main/example).
Expand Down
144 changes: 144 additions & 0 deletions docs/docs/getting-started/web.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
---
id: web
title: Web Support
sidebar_label: Web
slug: /getting-started/web
---

React Native Skia runs in a web browser thanks to [CanvasKit](https://skia.org/docs/user/modules/canvaskit/), a WebAssembly build of Skia.
The WebAssembly file is loaded asynchronously and has a size of 7.9MB.
While this is a substantial file size, you have control over the user experience: you can decide when to load Skia and how the loading experience should be.

We provide direct integrations with [Expo](#Expo) and [Remotion](#Remotion).
Below you will also find the manual installation steps to run the module on any React Native Web projects.

## Expo

Using React Native Skia on Expo web is reasonably straightforward.
We provide a script that will work well with the setup:
```bash
$ expo install @shopify/react-native-skia
$ yarn setup-skia-web
```

Once you are done, you need to pick your strategy to [Load Skia](#loading-skia).

If you are not using `react-native-reanimated`, webpack will output a warning because React Native Skia refers to Reanimated. We describe how to fix this warning [here](#manual-webpack-installation).

## Remotion

To use React Native Skia with Remotion, please follow [the following installation steps](https://remotion.dev/skia).

## Manual Webpack Installation

To run React Native Skia on Web, you need to do three things:
* Make sure that the WebAssembly file is available from the build system. This can easily be done using the [webpack copy plugin](https://webpack.js.org/plugins/copy-webpack-plugin/).
* Configure the build system to resolve the following two node modules: `fs` and `path`. One way to do it is to use the [node polyfill plugin](https://www.npmjs.com/package/node-polyfill-webpack-plugin).
* If you are not using the `react-native-reanimated`, Webpack will throw a warning since React Native Skia refers to that module.

So following is an example of a Webpack v5 configuration that supports React Native Skia.
These three steps can easily be adapted to your build system.

```tsx
import CopyPlugin from "copy-webpack-plugin";
import NodePolyfillPlugin from "node-polyfill-webpack-plugin";

const newConfiguration = {
...currentConfiguration,
plugins: [
...currentConfiguration.plugins,
// 1. Make the wasm file available to the build system
new CopyPlugin({
patterns: [
{
from: "node_modules/canvaskit-wasm/bin/full/canvaskit.wasm",
},
],
}),
// 2. Polyfill fs and path module from node
new NodePolyfillPlugin()
],
externals: {
...currentConfiguration.externals,
// 3. Avoid warning if reanimated is not present
"react-native-reanimated": "require('react-native-reanimated')",
"react-native-reanimated/lib/reanimated2/core":
"require('react-native-reanimated/lib/reanimated2/core')",
},
}
```

Last, you need to [load Skia](#loading-skia).

## Loading Skia

You need to have Skia fully loaded and initialized before importing the Skia module.
There are two ways you can control the way Skia should load:
* With `<WithSkiaWeb />`: using code-splitting to defer loading the components that import Skia.
* With `LoadSkiaWeb()`: deferring the root component registration until Skia is loaded.

### Using Code-Splitting

We provide a `<WithSkiaWeb>` component which leverages [code splitting](https://reactjs.org/docs/code-splitting.html). In the example below, we load Skia before loading the `MySkiaComponent` component.

```tsx
import React from 'react';
import { Text } from "react-native";
// Notice the import path `@shopify/react-native-skia/lib/module/web`
// This is important only to pull the code responsible for loading Skia.
// @ts-expect-error
import { WithSkiaWeb } from "@shopify/react-native-skia/lib/module/web";

export default function App() {
return (
<WithSkiaWeb
getComponent={() => import("./MySkiaComponent")}
fallback={<Text>Loading Skia...</Text>} />
);
}
```

### Using Defered Component Registration

We provide a `LoadSkiaWeb()` function you can use to load Skia before starting the React app.
This is the approach we use for Remotion, for instance.
The following is an example of an `index.web.js` file.

```tsx
// Notice the import path `@shopify/react-native-skia/lib/module/web`
// This is important only to pull the code responsible for loading Skia.
// @ts-expect-error
import { LoadSkiaWeb } from "@shopify/react-native-skia/lib/module/web";

// This is only needed on React Native Web
LoadSkiaWeb().then(async () => {
const App = (await import("./src/App")).default;
AppRegistry.registerComponent("Example", () => App);
});
```

## Unsupported Features

Below are the React Native Skia APIs which are not yet supported on React Native Web.
Some of these features are a work in progress, and some others will come later.

**Work in Progress**

* `ColorFilter::MakeLuma`
* `ImageFilter::MakeBlend`
* `ImageFilter::MakeDilate`
* `ImageFilter::MakeErode`
* `ImageFilter::MakeDropShadowOnly`
* `ImageFilter::MakeDropShadow`
* `ImageFilter::MakeDisplacementMap`
* `ImageFilter::MakeOffset`

**Coming soon**

* `Font::GetPath`
* `ImageFilter::MakeShader`
* `ShaderFilter`

**Unplanned**

* `ImageSvg`
6 changes: 5 additions & 1 deletion docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ const sidebars = {
collapsed: false,
type: "category",
label: "Getting started",
items: ["getting-started/installation", "getting-started/hello-world"],
items: [
"getting-started/installation",
"getting-started/hello-world",
"getting-started/web",
],
},
{
collapsed: true,
Expand Down
15 changes: 9 additions & 6 deletions package/scripts/setup-canvaskit.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env node
/* eslint-disable max-len */

/**
* A script to automate the setup of `@shopify/react-native-skia` for web.
Expand All @@ -8,7 +9,8 @@
* This script does the following:
* 1. Resolve the public path relative to wherever the script is being run.
* 2. Log out some useful info about the web setup, just in case anything goes wrong.
* 3. Resolve the installed wasm file `canvaskit-wasm/bin/full/canvaskit.wasm` from `@shopify/react-native-skia -> canvaskit`.
* 3. Resolve the installed wasm file `canvaskit-wasm/bin/full/canvaskit.wasm`
* from `@shopify/react-native-skia -> canvaskit`.
* 4. Recursively ensure the path exists and copy the file into the desired location.
*
*
Expand All @@ -34,7 +36,6 @@ function getWasmFilePath() {
try {
return require.resolve("canvaskit-wasm/bin/full/canvaskit.wasm");
} catch (error) {
// No idea how this could happen.
console.error(
`Could not find 'canvaskit-wasm'. Please install '@shopify/react-native-skia' and ensure it can be resolved from your project: ${process.cwd()}`
);
Expand All @@ -51,9 +52,6 @@ function getOutputFilePath() {
console.log(
`› Copying 'canvaskit.wasm' file to static folder:\n ${gray(output)}\n`
);
// A doc explaining what a public folder is -- React Native developers may not be familiar with the concept.
console.log(gray(`› Learn more: [TODO: Link to RNSkia docs]`));

return output;
}

Expand All @@ -67,5 +65,10 @@ function copyFile(from, to) {
(() => {
copyFile(getWasmFilePath(), getOutputFilePath());

console.log(lime(`› Success! You may need to restart your dev server`));
console.log(lime("› Success! You are almost there:"));
console.log(
gray(
"› To load React Native Skia Web, follow these instructions : https://shopify.github.io/react-native-skia/docs/getting-started/web"
)
);
})();
4 changes: 2 additions & 2 deletions package/src/renderer/__tests__/setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Container } from "../nodes";
import type { DrawingContext } from "../DrawingContext";
import { CanvasProvider } from "../useCanvas";
import { ValueApi } from "../../values/web";
import { LoadSkia } from "../../web/LoadSkia";
import { LoadSkiaWeb } from "../../web/LoadSkiaWeb";

export let Skia: ReturnType<typeof JsiSkApi>;

Expand All @@ -25,7 +25,7 @@ jest.mock("react-native", () => ({
export const nodeRequire = (uri: string) => fs.readFileSync(uri);

beforeAll(async () => {
await LoadSkia();
await LoadSkiaWeb();
Skia = JsiSkApi(global.CanvasKit);
});

Expand Down
4 changes: 2 additions & 2 deletions package/src/skia/__tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { LoadSkia } from "../../web/LoadSkia";
import { LoadSkiaWeb } from "../../web/LoadSkiaWeb";
import { Skia } from "../types";
import { JsiSkApi } from "../web";

let Skia: ReturnType<typeof JsiSkApi>;

beforeAll(async () => {
await LoadSkia();
await LoadSkiaWeb();
Skia = JsiSkApi(global.CanvasKit);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ declare global {
var CanvasKit: CanvasKitType;
}

export const LoadSkia = async () => {
export const LoadSkiaWeb = async () => {
const CanvasKit = await CanvasKitInit();
// The CanvasKit API is stored on the global object and used
// to create the JsiSKApi in the Skia.web.ts file.
global.CanvasKit = CanvasKit;
};

// We keep this function for backward compatibility
export const LoadSkia = LoadSkiaWeb;
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ import type { ComponentProps, ComponentType } from "react";
import React, { useMemo, lazy, Suspense } from "react";
import { Platform } from "react-native";

import { LoadSkia } from "./LoadSkia";
import { LoadSkiaWeb } from "./LoadSkiaWeb";

interface WithSkiaProps {
fallback: ComponentProps<typeof Suspense>["fallback"];
getComponent: () => Promise<{ default: ComponentType }>;
}

export const WithSkia = ({ getComponent, fallback }: WithSkiaProps) => {
export const WithSkiaWeb = ({ getComponent, fallback }: WithSkiaProps) => {
const Inner = useMemo(
() =>
lazy(async () => {
if (Platform.OS === "web") {
await LoadSkia();
await LoadSkiaWeb();
} else {
console.warn(
"<WithSkia /> is only necessary on web. Consider not using on native."
"<WithSkiaWeb /> is only necessary on web. Consider not using on native."
);
}
return getComponent();
Expand Down
4 changes: 2 additions & 2 deletions package/src/web/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "./LoadSkia";
export * from "./WithSkia";
export * from "./LoadSkiaWeb";
export * from "./WithSkiaWeb";

0 comments on commit f76a1c9

Please sign in to comment.