Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ lib/
node_modules/
yarn.lock
.idea
src/index.d.ts
6 changes: 3 additions & 3 deletions benchmark/results.js
Original file line number Diff line number Diff line change
@@ -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,
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 27 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -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<P extends object, A extends object> {
name?: string
props?: Partial<P>
Expand Down Expand Up @@ -100,6 +124,8 @@ const styled =
}
}

style = useStableStyle(style)

omitProps.forEach((excludeProp) => {
if ((restProps as Record<string, any>)[excludeProp]) {
delete (restProps as Record<string, any>)[excludeProp]
Expand Down
123 changes: 123 additions & 0 deletions test/memo-style.test.js
Original file line number Diff line number Diff line change
@@ -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 (
<View>
<MemoChild />
<View testID="count">{count}</View>
</View>
)
}

// eslint-disable-next-line no-unused-vars
let root
act(() => {
root = create(<Parent />)
})

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 (
<View>
<MemoChild big={false} />
<View testID="count">{count}</View>
</View>
)
}

// eslint-disable-next-line no-unused-vars
let root
act(() => {
root = create(<Parent />)
})

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 (
<View>
<MemoChild big={big} />
</View>
)
}

let root
act(() => {
root = create(<Parent />)
})

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 })
})
Loading