Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 3 additions & 4 deletions benchmark/results.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// eslint-disable-next-line no-unused-vars
const results = {

Check failure on line 1 in benchmark/results.js

View workflow job for this annotation

GitHub Actions / Lint

'results' is assigned a value but never used
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.012900377880803662,

Check failure on line 2 in benchmark/results.js

View workflow job for this annotation

GitHub Actions / Lint

Invalid group length in numeric value
Emotion: 0.01546536950109481,

Check failure on line 3 in benchmark/results.js

View workflow job for this annotation

GitHub Actions / Lint

Invalid group length in numeric value
'Styled Components': 0.021032191611905397,

Check failure on line 4 in benchmark/results.js

View workflow job for this annotation

GitHub Actions / Lint

Invalid group length in numeric value
}
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
12 changes: 12 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

Check failure on line 1 in rollup.config.js

View workflow job for this annotation

GitHub Actions / Lint

Replace `⏎const·rn·=·process.env.TARGET·===·'react-native';` with `const·rn·=·process.env.TARGET·===·'react-native'`
const rn = process.env.TARGET === 'react-native';

const external = ['react', rn && 'react-native'];

Check failure on line 4 in rollup.config.js

View workflow job for this annotation

GitHub Actions / Lint

Delete `;`
const input = rn ? 'src/index.js' : 'src/styled.js';

Check failure on line 5 in rollup.config.js

View workflow job for this annotation

GitHub Actions / Lint

Delete `;`
const file = rn ? 'lib/rn.js' : 'lib/index.js';

Check failure on line 6 in rollup.config.js

View workflow job for this annotation

GitHub Actions / Lint

Delete `;`

module.exports = {
external,
input,
output: { file, format: 'cjs', exports: 'named' },
}
25 changes: 24 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
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') {

Check failure on line 10 in src/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Expected blank line before this statement
const keysA = Object.keys(a)
return keysA.length === Object.keys(b).length && keysA.every((k) => a[k] === b[k])
}
return false

Check failure on line 14 in src/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Expected blank line before this statement
}

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 +121,8 @@
}
}

style = useStableStyle(style)

omitProps.forEach((excludeProp) => {
if ((restProps as Record<string, any>)[excludeProp]) {
delete (restProps as Record<string, any>)[excludeProp]
Expand Down
119 changes: 119 additions & 0 deletions test/memo-style.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { memo, useState } from 'react'
import { View } from 'react-native'
import { create, act } 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>
)
}

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

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