diff --git a/.gitignore b/.gitignore
index abbbd6e..2e5e672 100755
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ lib/
node_modules/
yarn.lock
.idea
+src/index.d.ts
\ No newline at end of file
diff --git a/benchmark/results.js b/benchmark/results.js
index 0893c9a..90ad7fa 100644
--- a/benchmark/results.js
+++ b/benchmark/results.js
@@ -1,6 +1,6 @@
// eslint-disable-next-line no-unused-vars
const results = {
- Shakl: 0.029_811_424_241_520_11,
- Emotion: 0.037_209_380_871_224_86,
- 'Styled Components': 0.046_611_316_695_394_33,
+ Shakl: 0.012_900_377_880_803_662,
+ Emotion: 0.015_465_369_501_094_81,
+ 'Styled Components': 0.021_032_191_611_905_397,
}
diff --git a/package.json b/package.json
index 4f7e354..e31d362 100755
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"license": "MIT",
"name": "shakl",
"description": "A utility to create styled components in React Native.",
- "version": "0.0.22",
+ "version": "1.0.0",
"main": "lib/index.js",
"react-native": "lib/rn.js",
"types": "lib/rn.d.ts",
diff --git a/src/index.ts b/src/index.ts
index 2b46605..164f3e4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,30 @@
-import React from 'react'
+import React, { useRef } from 'react'
import type { StyleProp } from 'react-native'
+const shallowEqual = (a: any, b: any): boolean => {
+ if (a === b) return true
+ if (!a || !b || typeof a !== typeof b) return false
+ if (Array.isArray(a)) {
+ return Array.isArray(b) && a.length === b.length && a.every((v, i) => shallowEqual(v, b[i]))
+ }
+
+ if (typeof a === 'object') {
+ const keysA = Object.keys(a)
+ return keysA.length === Object.keys(b).length && keysA.every((k) => a[k] === b[k])
+ }
+
+ return false
+}
+
+const useStableStyle = (style: any) => {
+ const ref = useRef(style)
+ if (!shallowEqual(ref.current, style)) {
+ ref.current = style
+ }
+
+ return ref.current
+}
+
export interface Config
{
name?: string
props?: Partial
@@ -100,6 +124,8 @@ const styled =
}
}
+ style = useStableStyle(style)
+
omitProps.forEach((excludeProp) => {
if ((restProps as Record)[excludeProp]) {
delete (restProps as Record)[excludeProp]
diff --git a/test/memo-style.test.js b/test/memo-style.test.js
new file mode 100644
index 0000000..50063b0
--- /dev/null
+++ b/test/memo-style.test.js
@@ -0,0 +1,123 @@
+import React, { memo, useState } from 'react'
+import { View } from 'react-native'
+import { act, create } from 'react-test-renderer'
+
+import s from '../src/rn'
+
+it('does not re-render memoized child when parent re-renders with same style', () => {
+ let childRenderCount = 0
+
+ const StyledChild = s.View({ flex: 1 })
+ const MemoChild = memo(StyledChild)
+
+ const Parent = () => {
+ const [count, setCount] = useState(0)
+ Parent.setCount = setCount
+ return (
+
+
+ {count}
+
+ )
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ let root
+ act(() => {
+ root = create()
+ })
+
+ childRenderCount = 0
+
+ // spy on re-renders by wrapping createElement
+ const originalCreateElement = React.createElement
+ React.createElement = (...args) => {
+ if (args[0] === View && args[1]?.style?.flex === 1) {
+ childRenderCount++
+ }
+
+ return originalCreateElement(...args)
+ }
+
+ act(() => {
+ Parent.setCount(1)
+ })
+
+ React.createElement = originalCreateElement
+
+ // MemoChild should not re-render because useStableStyle returns the same reference
+ expect(childRenderCount).toBe(0)
+})
+
+it('does not re-render memoized child when dynamic style resolves to same values', () => {
+ let childRenderCount = 0
+
+ const StyledChild = s.View((p) => ({ padding: p.big ? 20 : 10 }))
+ const MemoChild = memo(StyledChild)
+
+ const Parent = () => {
+ const [count, setCount] = useState(0)
+ Parent.setCount = setCount
+ return (
+
+
+ {count}
+
+ )
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ let root
+ act(() => {
+ root = create()
+ })
+
+ childRenderCount = 0
+
+ const originalCreateElement = React.createElement
+ React.createElement = (...args) => {
+ if (args[0] === View && args[1]?.style?.padding === 10) {
+ childRenderCount++
+ }
+
+ return originalCreateElement(...args)
+ }
+
+ act(() => {
+ Parent.setCount(1)
+ })
+
+ React.createElement = originalCreateElement
+
+ expect(childRenderCount).toBe(0)
+})
+
+it('re-renders memoized child when style actually changes', () => {
+ const StyledChild = s.View((p) => ({ padding: p.big ? 20 : 10 }))
+ const MemoChild = memo(StyledChild)
+
+ const Parent = () => {
+ const [big, setBig] = useState(false)
+ Parent.setBig = setBig
+ return (
+
+
+
+ )
+ }
+
+ let root
+ act(() => {
+ root = create()
+ })
+
+ const tree1 = root.toJSON()
+ expect(tree1.children[0].props.style).toEqual({ padding: 10 })
+
+ act(() => {
+ Parent.setBig(true)
+ })
+
+ const tree2 = root.toJSON()
+ expect(tree2.children[0].props.style).toEqual({ padding: 20 })
+})