Skip to content

Commit

Permalink
Deprecate withRef in favor of forwardRef and React.forwardRef
Browse files Browse the repository at this point in the history
Deprecate withRef and use forwardRef prop instead.
When forwardRef is true, the ref passed to the injected component
will be passed down to the wrapped component.

Display a invariant message when withRef is used.

Closes #1211.
  • Loading branch information
mrijke committed May 3, 2019
1 parent 4109ef1 commit 3093755
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 147 deletions.
57 changes: 28 additions & 29 deletions src/components/withIntl.js
Expand Up @@ -8,62 +8,61 @@ function getDisplayName(Component) {
}

const IntlContext = createContext(null);
const {
Consumer: IntlConsumer,
Provider: IntlProvider
} = IntlContext
const {Consumer: IntlConsumer, Provider: IntlProvider} = IntlContext;

export const Provider = IntlProvider
export const Context = IntlContext
export const Provider = IntlProvider;
export const Context = IntlContext;

export default function withIntl(WrappedComponent, options = {}) {
const {
intlPropName = 'intl',
forwardRef = false,
// DEPRECATED - use forwardRef and ref on injected component
withRef = false,
enforceContext = true
enforceContext = true,
} = options;

invariant(
!withRef,
'[React Intl] withRef and getWrappedInstance() are deprecated, ' +
'use forwardRef and set a ref directly on the wrapped component'
);

class withIntl extends Component {
static displayName = `withIntl(${getDisplayName(WrappedComponent)})`;
static WrappedComponent = WrappedComponent;

wrappedInstance = (ref) => {
this.wrappedInstance.current = ref;
}

getWrappedInstance() {
invariant(
withRef,
'[React Intl] To access the wrapped instance, ' +
'the `{withRef: true}` option must be set when calling: ' +
'`withIntl()`'
);

return this.wrappedInstance.current;
}

render () {

render() {
return (
<IntlConsumer>
{(intl) => {
{intl => {
if (enforceContext) {
invariantIntlContext({ intl });
invariantIntlContext({intl});
}

return (
<WrappedComponent
{...{
...this.props,
[intlPropName]: intl
[intlPropName]: intl,
}}
ref={withRef ? this.wrappedInstance : null}
ref={forwardRef ? this.props.forwardedRef : null}
/>
);
}}
</IntlConsumer>
)
);
}
}

if (forwardRef) {
return hoistNonReactStatics(
React.forwardRef((props, ref) =>
React.createElement(withIntl, {...props, forwardedRef: ref})
),
WrappedComponent
);
}

return hoistNonReactStatics(withIntl, WrappedComponent);
}
241 changes: 123 additions & 118 deletions test/unit/components/withIntl.js
Expand Up @@ -2,144 +2,149 @@ import expect, {spyOn} from 'expect';
import React from 'react';
import {mount} from 'enzyme';
import {intlShape} from '../../../src/types';
import IntlProvider from '../../../src/components/provider'
import IntlProvider from '../../../src/components/provider';
import withIntl from '../../../src/components/withIntl';

const mountWithProvider = (el) => mount(
<IntlProvider locale='en'>
{ el }
</IntlProvider>
)
const mountWithProvider = el =>
mount(<IntlProvider locale="en">{el}</IntlProvider>);

describe('withIntl()', () => {
let Wrapped;
let rendered;

beforeEach(() => {
Wrapped = () => <div />;
Wrapped.displayName = 'Wrapped';
Wrapped.propTypes = {
intl: intlShape.isRequired,
};
Wrapped.someNonReactStatic = {
foo: true
};
rendered = null;
let Wrapped;
let rendered;

beforeEach(() => {
Wrapped = () => <div />;
Wrapped.displayName = 'Wrapped';
Wrapped.propTypes = {
intl: intlShape.isRequired,
};
Wrapped.someNonReactStatic = {
foo: true,
};
rendered = null;
});

afterEach(() => {
rendered && rendered.unmount();
});

it('allows introspection access to the wrapped component', () => {
expect(withIntl(Wrapped).WrappedComponent).toBe(Wrapped);
});

it('hoists non-react statics', () => {
expect(withIntl(Wrapped).someNonReactStatic.foo).toBe(true);
});

describe('displayName', () => {
it('is descriptive by default', () => {
expect(withIntl(() => null).displayName).toBe('withIntl(Component)');
});

afterEach(() => {
rendered && rendered.unmount();
})

it('allows introspection access to the wrapped component', () => {
expect(withIntl(Wrapped).WrappedComponent).toBe(Wrapped);
it("includes `WrappedComponent`'s `displayName`", () => {
Wrapped.displayName = 'Foo';
expect(withIntl(Wrapped).displayName).toBe('withIntl(Foo)');
});

it('hoists non-react statics',() => {
expect(withIntl(Wrapped).someNonReactStatic.foo).toBe(true)
})

describe('displayName', () => {
it('is descriptive by default', () => {
expect(withIntl(() => null).displayName).toBe('withIntl(Component)');
});

it('includes `WrappedComponent`\'s `displayName`', () => {
Wrapped.displayName = 'Foo';
expect(withIntl(Wrapped).displayName).toBe('withIntl(Foo)');
});
});

it('throws when <IntlProvider> is missing from ancestry', () => {
const Injected = withIntl(Wrapped);

const consoleError = spyOn(console, 'error'); // surpress console error from JSDom
expect(() => (rendered = mount(<Injected />))).toThrow(
'[React Intl] Could not find required `intl` object. <IntlProvider> needs to exist in the component ancestry.'
);
consoleError.restore();
});

it('renders <WrappedComponent> with `intl` prop', () => {
const Injected = withIntl(Wrapped);

rendered = mountWithProvider(<Injected />);
const wrappedComponent = rendered.find(Wrapped);
// React 16 renders different in the wrapper
const intlProvider = rendered.find(IntlProvider).childAt(0);

expect(wrappedComponent.prop('intl')).toBe(
intlProvider.instance().getContext()
);
});

it('propagates all props to <WrappedComponent>', () => {
const Injected = withIntl(Wrapped);
const props = {
foo: 'bar',
};

rendered = mountWithProvider(<Injected {...props} />);
const wrappedComponent = rendered.find(Wrapped);

Object.keys(props).forEach(key => {
expect(wrappedComponent.prop(key)).toBe(props[key]);
});
});

it('throws when <IntlProvider> is missing from ancestry', () => {
const Injected = withIntl(Wrapped);
describe('options', () => {
describe('intlPropName', () => {
it("sets <WrappedComponent>'s `props[intlPropName]` to `context.intl`", () => {
const propName = 'myIntl';
Wrapped.propTypes = {
[propName]: intlShape.isRequired,
};
const Injected = withIntl(Wrapped, {
intlPropName: propName,
});

const consoleError = spyOn(console, 'error'); // surpress console error from JSDom
expect(() => rendered = mount(<Injected />)).toThrow(
'[React Intl] Could not find required `intl` object. <IntlProvider> needs to exist in the component ancestry.'
rendered = mountWithProvider(<Injected />);
const wrapped = rendered.find(Wrapped);
// React 16 renders differently
const intlProvider = React.version.startsWith('16')
? rendered.find(IntlProvider).childAt(0)
: rendered
.find(IntlProvider)
.childAt(0)
.childAt(0);

expect(wrapped.prop(propName)).toBe(
intlProvider.instance().getContext()
);
consoleError.restore();
});
});

it('renders <WrappedComponent> with `intl` prop', () => {
const Injected = withIntl(Wrapped);

rendered = mountWithProvider(<Injected />);
const wrappedComponent = rendered.find(Wrapped);
// React 16 renders different in the wrapper
const intlProvider = rendered.find(IntlProvider).childAt(0);

expect(
wrappedComponent.prop('intl')
).toBe(intlProvider.instance().getContext());
describe('withRef', () => {
it('throws when true', () => {
expect(() => withIntl(Wrapped, {withRef: true})).toThrow(
'[React Intl] withRef and getWrappedInstance() are deprecated, ' +
'use forwardRef and set a ref directly on the wrapped component'
);
});
});

it('propagates all props to <WrappedComponent>', () => {
describe('forwardRef', () => {
it("doesn't forward the ref when forwardRef is `false`", () => {
const Injected = withIntl(Wrapped);
const props = {
foo: 'bar'
}

rendered = mountWithProvider(<Injected {...props} />);
const wrappedComponent = rendered.find(Wrapped);

Object.keys(props).forEach((key) => {
expect(wrappedComponent.prop(key)).toBe(props[key]);
})
});

describe('options', () => {
describe('intlPropName', () => {
it('sets <WrappedComponent>\'s `props[intlPropName]` to `context.intl`', () => {
const propName = 'myIntl';
Wrapped.propTypes = {
[propName]: intlShape.isRequired
};
const Injected = withIntl(Wrapped, {
intlPropName: propName
});

rendered = mountWithProvider(<Injected />);
const wrapped = rendered.find(Wrapped);
// React 16 renders differently
const intlProvider = React.version.startsWith('16') ?
rendered.find(IntlProvider).childAt(0) : rendered.find(IntlProvider).childAt(0).childAt(0);

expect(wrapped.prop(propName)).toBe(
intlProvider.instance().getContext()
);
});
});

describe('withRef', () => {
it('throws when `false` and getWrappedInstance() is called', () => {
const Injected = withIntl(Wrapped);
const wrapperRef = React.createRef();

rendered = mountWithProvider(<Injected />);
const wrapper = rendered.find(Injected);
rendered = mountWithProvider(<Injected ref={wrapperRef} />);
const wrapper = rendered.find(Injected);

expect(() => wrapper.instance().getWrappedInstance()).toThrow(
'[React Intl] To access the wrapped instance, the `{withRef: true}` option must be set when calling: `withIntl()`'
);
});
expect(wrapperRef.current).toBe(wrapper.instance());
});

it('does not throw when `true` getWrappedInstance() is called', () => {
Wrapped = class extends React.Component {
render () {
return null
}
}

const Injected = withIntl(Wrapped, { withRef: true });
it('forwards the ref properly to the wrapped component', () => {
Wrapped = class extends React.Component {
render() {
return null;
}
};
const Injected = withIntl(Wrapped, {forwardRef: true});
const wrapperRef = React.createRef();

rendered = mountWithProvider(<Injected />);
const wrapper = rendered.find(Injected);
const wrapped = rendered.find(Wrapped);
rendered = mountWithProvider(<Injected ref={wrapperRef} />);
const wrapped = rendered.find(Wrapped);

expect(() => wrapper.instance().getWrappedInstance())
.toNotThrow();
expect(wrapper.instance().getWrappedInstance())
.toBe(wrapped.instance());
});
});
expect(wrapperRef.current).toBe(wrapped.instance());
});
});
});
});

0 comments on commit 3093755

Please sign in to comment.