Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support React.memo #19

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,11 @@ component](https://facebook.github.io/react/docs/components-and-props.html)
elements, the element type is compared by identity. After deserialization the
element types are compared by function name.

Component elements are formatted with a ⍟ character after the element
name. Properties and children are formatted by [Concordance](https://github.com/concordancejs/concordance).
[Memoized elements](https://reactjs.org/docs/react-api.html#reactmemo) are
supported, however different memoizations of the same function are considered
equal if used with the same properties.

Memoized elements are formatted with a ⍝ character after the element
name. Component elements are formatted with a ⍟ character. Properties and
children are formatted by
[Concordance](https://github.com/concordancejs/concordance).
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ exports.serializerVersion = 2
exports.theme = {
react: {
functionType: '\u235F',
memoizedType: `\u235D`,
openTag: {
start: '<',
end: '>',
Expand Down
37 changes: 28 additions & 9 deletions lib/elementFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const diffShallow = require('./diffShallow')
const escapeText = require('./escapeText')

const FRAGMENT_NAME = Symbol.for('react.fragment')
const MEMO_TYPE = Symbol.for('react.memo')

function factory (api, reactTags) {
const tag = Symbol('@concordance/react.ElementValue')
Expand Down Expand Up @@ -62,7 +63,11 @@ function factory (api, reactTags) {
function describe (props) {
const element = props.value

const type = element.type
let type = element.type
const hasMemoizedType = type.$$typeof === MEMO_TYPE
// Dereference underlying type if memoized.
if (hasMemoizedType) type = type.type

const hasTypeFn = typeof type === 'function'
const typeFn = hasTypeFn ? type : null
const name = hasTypeFn ? type.displayName || type.name : type
Expand All @@ -78,6 +83,7 @@ function factory (api, reactTags) {

return new DescribedElementValue(Object.assign({
children,
hasMemoizedType,
hasProperties,
hasTypeFn,
name,
Expand All @@ -96,6 +102,7 @@ function factory (api, reactTags) {
super(props)
this.isFragment = props.name === FRAGMENT_NAME
this.name = props.name
this.hasMemoizedType = props.hasMemoizedType
this.hasProperties = props.hasProperties
this.hasTypeFn = props.hasTypeFn

Expand All @@ -110,13 +117,15 @@ function factory (api, reactTags) {

formatName (theme) {
const formatted = api.wrapFromTheme(theme.react.tagName, this.isFragment ? 'React.Fragment' : this.name)
return this.hasTypeFn
? formatted + theme.react.functionType
: formatted
if (this.hasMemoizedType) return formatted + theme.react.memoizedType
if (this.hasTypeFn) return formatted + theme.react.functionType
return formatted
}

compareNames (expected) {
return this.name === expected.name && this.hasTypeFn === expected.hasTypeFn
return this.name === expected.name &&
this.hasMemoizedType === expected.hasMemoizedType &&
this.hasTypeFn === expected.hasTypeFn
}

formatShallow (theme, indent) {
Expand Down Expand Up @@ -216,7 +225,14 @@ function factory (api, reactTags) {
}

serialize () {
return [this.isFragment, this.isFragment ? null : this.name, this.hasProperties, this.hasTypeFn, super.serialize()]
return [
this.isFragment,
this.isFragment ? null : this.name,
this.hasMemoizedType,
this.hasProperties,
this.hasTypeFn,
super.serialize()
]
}
}
Object.defineProperty(ElementValue.prototype, 'tag', {value: tag})
Expand Down Expand Up @@ -325,11 +341,14 @@ function factory (api, reactTags) {
function DeserializedMixin (base) {
return class extends api.DeserializedMixin(base) {
constructor (state, recursor) {
super(state[4], recursor)
const legacy = state.length === 5
super(state[legacy ? 4 : 5], recursor)

this.isFragment = state[0]
this.name = this.isFragment ? FRAGMENT_NAME : state[1]
this.hasProperties = state[2]
this.hasTypeFn = state[3]
this.hasMemoizedType = legacy ? false : state[2]
this.hasProperties = state[legacy ? 2 : 3]
this.hasTypeFn = state[legacy ? 3 : 4]
}

createRecursor () {
Expand Down
24 changes: 24 additions & 0 deletions test/backcompat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import test from 'ava'
import {compareDescriptors, describe, deserialize} from 'concordance'

import React from 'react'

import plugin from '..'
import HelloMessage from './fixtures/react/HelloMessage'

const plugins = [plugin]

const equalsSerialization = (t, buffer, getValue) => {
const expected = describe(getValue(), {plugins})

const deserialized = deserialize(buffer, {plugins})
t.true(
compareDescriptors(deserialized, expected),
'the deserialized descriptor equals the expected value')
}

test('element serialization before React.memo support was added',
equalsSerialization,
Buffer.from('AwAfAAAAAQERARJAY29uY29yZGFuY2UvcmVhY3QBAgEBAQEBAlwAAABiAAAAEwEFEBEBDEhlbGxvTWVzc2FnZQ8PEwEGEQEGT2JqZWN0AQERAQZPYmplY3QQEBAAAQ0AAQEAAQ8AEwECFAEDAAEFEQEEbmFtZRQBAwABBREBBEpvaG4=', 'base64'), // eslint-disable-line max-len
() => <HelloMessage name='John' />
)
11 changes: 10 additions & 1 deletion test/compare.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React from 'react'
import renderer from 'react-test-renderer'

import plugin from '..'
import HelloMessage from './fixtures/react/HelloMessage'
import HelloMessage, {MemoizedHelloMessage} from './fixtures/react/HelloMessage'

const plugins = [plugin]
const render = value => renderer.create(value).toJSON()
Expand All @@ -30,6 +30,15 @@ test('react elements', macros,
() => React.createElement('Foo'),
() => React.createElement('Bar'))

test('memoized elements', macros,
() => <MemoizedHelloMessage name='John' />,
() => <MemoizedHelloMessage name='Olivia' />)

test('different memoizations are equal', t => {
const SecondHelloMessage = React.memo(HelloMessage)
t.true(concordance.compare(<MemoizedHelloMessage name='John' />, <SecondHelloMessage name='John' />, {plugins}).pass)
})

test('fragments', macros,
() => <React.Fragment><HelloMessage name='John' /></React.Fragment>,
() => <React.Fragment><HelloMessage name='Olivia' /></React.Fragment>)
Expand Down
10 changes: 9 additions & 1 deletion test/diff.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React from 'react'
import renderer from 'react-test-renderer'

import plugin from '..'
import HelloMessage from './fixtures/react/HelloMessage'
import HelloMessage, {MemoizedHelloMessage} from './fixtures/react/HelloMessage'

const plugins = [plugin]

Expand All @@ -28,6 +28,14 @@ test('react elements', macros,
() => <strong>arm</strong>,
() => <em>arm</em>)

test('memoized elements', macros,
() => <MemoizedHelloMessage name='John' />,
() => <MemoizedHelloMessage name='Olivia' />)

test('memoized elements against non-memoized elements', macros,
() => <MemoizedHelloMessage name='John' />,
() => <HelloMessage name='John' />)

test('fragments', macros,
() => <React.Fragment><HelloMessage name='John' /></React.Fragment>,
() => <React.Fragment><HelloMessage name='Olivia' /></React.Fragment>)
Expand Down
2 changes: 2 additions & 0 deletions test/fixtures/react/HelloMessage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export default class HelloMessage extends React.Component {
return <div>Hello <NameHighlight name={this.props.name} /></div>
}
}

export const MemoizedHelloMessage = React.memo(HelloMessage)
3 changes: 2 additions & 1 deletion test/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React from 'react'
import renderer from 'react-test-renderer'

import plugin from '..'
import HelloMessage from './fixtures/react/HelloMessage'
import HelloMessage, {MemoizedHelloMessage} from './fixtures/react/HelloMessage'

const plugins = [plugin]
const format = (value, options) => concordance.format(value, Object.assign({plugins}, options))
Expand All @@ -22,6 +22,7 @@ snapshotRendered.title = prefix => `formats rendered ${prefix}`
const macros = [snapshot, snapshotRendered]

test('react elements', macros, () => <HelloMessage name='John' />)
test('memoized elements', macros, () => <MemoizedHelloMessage name='John' />)
test('fragments', macros, () => <React.Fragment><HelloMessage name='John' /></React.Fragment>)
test('object properties', macros, () => {
return React.createElement('Foo', {object: {baz: 'thud'}})
Expand Down
12 changes: 2 additions & 10 deletions test/serialize-and-encode.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react'
import renderer from 'react-test-renderer'

import plugin from '..'
import HelloMessage from './fixtures/react/HelloMessage'
import HelloMessage, {MemoizedHelloMessage} from './fixtures/react/HelloMessage'

const plugins = [plugin]

Expand Down Expand Up @@ -46,15 +46,7 @@ useDeserializedRendered.title = prefix => `deserialized rendered ${prefix} is eq
const macros = [useDeserialized, useDeserializedRendered]

test('react elements', macros, () => <HelloMessage name='John' />)
// TODO: Combine next two tests with `macros` array
test.failing('memoized react elements', useDeserialized, () => {
const MemoizedHelloMessage = React.memo(HelloMessage)
return <MemoizedHelloMessage name='John' />
})
test('memoized react elements', useDeserializedRendered, () => {
const MemoizedHelloMessage = React.memo(HelloMessage)
return <MemoizedHelloMessage name='John' />
})
test('memoized elements', macros, () => <MemoizedHelloMessage name='John' />)
test('fragments', macros, () => <React.Fragment><HelloMessage name='John' /></React.Fragment>)
test('object properties', macros, () => {
return React.createElement('Foo', {object: {baz: 'thud'}})
Expand Down
41 changes: 41 additions & 0 deletions test/snapshots/diff.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,24 @@ Generated by [AVA](https://ava.li).
</div>␊
</div>`

## diffs memoized elements

> Snapshot 1

` <HelloMessage⍝␊
- name="John"␊
+ name="Olivia"␊
/>`

## diffs memoized elements against non-memoized elements

> Snapshot 1

`- <HelloMessage⍝␊
+ <HelloMessage⍟␊
name="John"␊
/>`

## diffs multiline string properties

> Snapshot 1
Expand Down Expand Up @@ -447,6 +465,29 @@ Generated by [AVA](https://ava.li).
</div>␊
</div>`

## diffs rendered memoized elements

> Snapshot 1

` <div>␊
Hello ␊
<mark>␊
- John␊
+ Olivia␊
</mark>␊
</div>`

## diffs rendered memoized elements against non-memoized elements

> Snapshot 1

` <div>␊
Hello ␊
<mark>␊
John␊
</mark>␊
</div>`

## diffs rendered multiline string properties

> Snapshot 1
Expand Down
Binary file modified test/snapshots/diff.js.snap
Binary file not shown.
19 changes: 19 additions & 0 deletions test/snapshots/format.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ Generated by [AVA](https://ava.li).
</div>␊
</div>`

## formats memoized elements

> Snapshot 1

`<HelloMessage⍝␊
name="John"␊
/>`

## formats multiline string properties

> Snapshot 1
Expand Down Expand Up @@ -184,6 +192,17 @@ Generated by [AVA](https://ava.li).
</div>␊
</div>`

## formats rendered memoized elements

> Snapshot 1

`<div>␊
Hello ␊
<mark>␊
John␊
</mark>␊
</div>`

## formats rendered multiline string properties

> Snapshot 1
Expand Down
Binary file modified test/snapshots/format.js.snap
Binary file not shown.