To develop react-native-skia, you can build the skia libraries on your computer. Alternatively, you can use the pre-built binaries.
The Skia prebuilt binaries are installed as npm dependencies (react-native-skia-android, react-native-skia-apple-*). The native build systems (Gradle, CocoaPods) automatically resolve these packages.
- Checkout submodules:
git submodule update --init --recursive - Install dependencies:
yarn
If you have Android Studio installed, make sure $ANDROID_NDK is available.
ANDROID_NDK=/Users/username/Library/Android/sdk/ndk/<version> for instance.
If the NDK is not installed, you can install it via Android Studio by going to the menu File > Project Structure.
And then the SDK Location section. It will show you the NDK path, or the option to Download it if you don't have it installed.
- Checkout submodules:
git submodule update --init --recursive - Install dependencies:
yarn - Go to the package folder:
cd packages/skia - Build the Skia libraries:
yarn build-skia(this can take a while) - Copy Skia headers:
yarn copy-skia-headers
If a new version of Skia is included in an upgrade of this library, you need to perform a few extra steps before continuing:
- Update submodules:
git submodule update --recursive --remote - Clean Skia:
yarn clean-skia - Build Skia:
yarn build-skia - Copy Skia Headers:
yarn copy-skia-headers - Run pod install in the example project
- Run the commands in the Building section
- Build the Android binaries with
yarn build-skia-android - Build the NPM package with
yarn build-npm
Publish the NPM package manually. The output is found in the dist folder.
- Install Cocoapods in the example/ios folder
cd example/ios && pod install && cd ..
When making contributions to the project, an important part is testing.
In the package folder, we have several scripts set up to help you maintain the quality of the codebase and test your changes:
yarn lintβ Lints the code for potential errors and to ensure consistency with our coding standards.yarn tscβ Runs the TypeScript compiler to check for typing issues.yarn testβ Executes the unit tests to ensure existing features work as expected after changes.yarn e2eβ Runs end-to-end tests. For these tests to run properly, you need to have the example app running. Useyarn iosoryarn androidin theexamplefolder and navigate to the Tests screen within the app.
To ensure the best reliability, we encourage running end-to-end tests before submitting your changes:
- Start the example app:
cd example
yarn ios # or yarn android for Android testingOnce the app is open in your simulator or device, press the "Tests" item at the bottom of the list.
- With the example app running and the Tests screen open, run the following command in the
packagefolder:
yarn e2eThis will run through the automated tests and verify that your changes have not introduced any regressions. You can also run a particular using the following command:
E2E=true yarn test -i e2e/ColorsContributing end-to-end tests to React Native Skia is extremely useful. Below you'll find guidelines for writing tests using the eval, draw, and drawOffscreen commands.
e2e tests are located in the package/__tests__/e2e/ directory. You can create a file there or add a new test to an existing file depending on what is most sensible.
When looking to contribute a new test, you can refer to existing tests to see how these can be built.
The eval command is used to test Skia's imperative API. It requires a pure function that invokes Skia operations and returns a serialized result.
it("should generate commands properly", async () => {
const result = await surface.eval((Skia) => {
const path = Skia.Path.Make();
path.lineTo(30, 30);
return path.toCmds();
});
expect(result).toEqual([[0, 0, 0], [1, 30, 30]]);
});Both the eval and draw commands require a function that will be executed in an isolated context, so the functions must be pure (without external dependencies) and serializable. You can use the second parameter to provide extra data to that function.
it("should generate commands properly", async () => {
// Referencing the SVG variable directly in the tests would fail
// as the function wouldn't be able to run in an isolated context
const svg = "M 0 0, L 30 30";
const result = await surface.eval((Skia, ctx) => {
const path = Skia.Path.MakeFromSVGString(ctx.svg);
return path.toCmds();
}, { svg });
expect(result).toEqual([[0, 0, 0], [1, 30, 30]]);
});A second option is to use the draw command where you can test the Skia components and get the resulting image:
it("Path with default fillType", async () => {
const { Skia } = importSkia();
const path = star(Skia);
const img = await surface.draw(
<>
<Fill color="white" />
<Path path={path} style="stroke" strokeWidth={4} color="#3EB489" />
<Path path={path} color="lightblue" />
</>
);
checkImage(image, "snapshots/drawings/path.png");
});Finally, you can use drawOffscreen to receive a canvas object as parameter. You will also get the resulting image:
it("Should draw cyan", async () => {
const image = await surface.drawOffscreen(
(Skia, canvas, { size }) => {
canvas.drawColor(Skia.Color("cyan"));
}
);
checkImage(image, "snapshots/cyan.png");
});Again, since eval, draw, and drawOffscreen serialize the function's content, avoid any external dependencies that can't be serialized.
This guide explains how to add new components to the React Native Skia scene graph system.
1. Drawing Commands (like Skottie)
- Draw content directly to the canvas
- Examples: Skottie, Circle, Rect, Text
2. Context Declarations (like ImageFilter)
- Modify the rendering context for child components
- Examples: ImageFilter, ColorFilter, MaskFilter, Shader
π src/dom/types/Drawings.ts
// Add import for Skia types
import { SkImageFilter } from "../../skia/types";
// Define props interface
export interface ImageFilterProps extends GroupProps {
imageFilter: SkImageFilter;
}π src/dom/types/NodeType.ts
export const enum NodeType {
// ... existing types
ImageFilter = "skImageFilter",
}π src/renderer/components/ImageFilter.tsx
import React from "react";
import type { ImageFilterProps } from "../../dom/types";
import type { SkiaProps } from "../processors";
export const ImageFilter = (props: SkiaProps<ImageFilterProps>) => {
return <skImageFilter {...props} />;
};π src/renderer/components/index.ts
export * from "./ImageFilter";π cpp/api/recorder/Convertor.h
For components that use complex Skia types (like SkImageFilter, skottie::Animation, etc.), add a template specialization to convert JSI values to native types:
template <>
sk_sp<SkImageFilter> getPropertyValue(jsi::Runtime &runtime,
const jsi::Value &value) {
if (value.isObject() && value.asObject(runtime).isHostObject(runtime)) {
auto ptr = std::dynamic_pointer_cast<JsiSkImageFilter>(
value.asObject(runtime).asHostObject(runtime));
if (ptr != nullptr) {
return ptr->getObject();
}
} else if (value.isNull()) {
return nullptr;
}
throw std::runtime_error(
"Expected JsiSkImageFilter object or null for the imageFilter property.");
}π cpp/api/recorder/ImageFilters.h
struct ImageFilterCmdProps {
sk_sp<SkImageFilter> imageFilter;
};
class ImageFilterCmd : public Command {
private:
ImageFilterCmdProps props;
public:
ImageFilterCmd(jsi::Runtime &runtime, const jsi::Object &object,
Variables &variables)
: Command(CommandType::PushImageFilter, "skImageFilter") {
convertProperty(runtime, object, "imageFilter", props.imageFilter, variables);
}
void pushImageFilter(DrawingCtx *ctx) {
ctx->imageFilters.push_back(props.imageFilter);
}
};struct SkottieCmdProps {
sk_sp<skottie::Animation> animation;
float frame;
};
class SkottieCmd : public Command {
private:
SkottieCmdProps props;
public:
SkottieCmd(jsi::Runtime &runtime, const jsi::Object &object,
Variables &variables)
: Command(CommandType::DrawSkottie) {
convertProperty(runtime, object, "animation", props.animation, variables);
convertProperty(runtime, object, "frame", props.frame, variables);
}
void draw(DrawingCtx *ctx) {
props.animation->seekFrame(props.frame);
props.animation->render(ctx->canvas);
}
};π cpp/api/recorder/RNRecorder.h
// Add to appropriate push method
void pushImageFilter(jsi::Runtime &runtime, const std::string &nodeType,
const jsi::Object &props) {
// ... existing registrations
} else if (nodeType == "skImageFilter") {
commands.push_back(
std::make_unique<ImageFilterCmd>(runtime, props, variables));
}
}π cpp/api/recorder/RNRecorder.h
// In the play method's switch statement
case CommandType::PushImageFilter: {
auto nodeType = cmd->nodeType;
// ... existing cases
} else if (nodeType == "skImageFilter") {
auto *imageFilterCmd = static_cast<ImageFilterCmd *>(cmd.get());
imageFilterCmd->pushImageFilter(ctx);
}
break;
}π src/sksg/Node.ts
For new general component types (like ImageFilter, ColorFilter, etc.), add them to the appropriate classification function:
// For context declarations like ImageFilter
export const isImageFilter = (type: NodeType) => {
"worklet";
return (
type === NodeType.ImageFilter || // Add your new general type here
type === NodeType.OffsetImageFilter ||
// ... other specific types
);
};π src/renderer/__tests__/e2e/ImageFilter.spec.tsx
import React from "react";
import { checkImage, docPath } from "../../../__tests__/setup";
import { importSkia, surface } from "../setup";
import { ImageFilter, Circle, Group } from "../../components";
import { TileMode } from "../../../skia/types";
describe("ImageFilter", () => {
it("Should render ImageFilter component with blur filter", async () => {
const { Skia } = importSkia();
const blurFilter = Skia.ImageFilter.MakeBlur(10, 10, TileMode.Clamp, null);
const img = await surface.draw(
<Group>
<ImageFilter imageFilter={blurFilter}>
<Circle cx={50} cy={50} r={30} color="red" />
</ImageFilter>
</Group>
);
checkImage(img, docPath("image-filter/blur-filter.png"));
});
});# Check TypeScript compilation
yarn tsc --noEmit
# Create test image directory
mkdir -p apps/docs/static/img/your-component/
# Run tests
yarn test src/renderer/__tests__/e2e/YourComponent.spec.tsxThis pattern allows you to add both types of components consistently to the React Native Skia scene graph system, maintaining clean separation between React component layer, type definitions, and native C++ implementation.