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 16, 2019
1 parent 6923cfd commit 842e03e
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 147 deletions.
59 changes: 29 additions & 30 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;

class withIntl extends Component {
invariant(
!withRef,
'[React Intl] withRef and getWrappedInstance() are deprecated, ' +
"instead use the 'forwardRef' option and create 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>
)
);
}
}

return hoistNonReactStatics(withIntl, WrappedComponent);
if (forwardRef) {
return hoistNonReactStatics(
React.forwardRef((props, ref) => (
<WithIntl {...props} forwardedRef={ref} />
)),
WrappedComponent
);
}

return hoistNonReactStatics(WithIntl, WrappedComponent);
}
234 changes: 117 additions & 117 deletions test/unit/components/withIntl.js
Expand Up @@ -2,144 +2,144 @@ 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('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('renders <WrappedComponent> with `intl` prop', () => {
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,
});

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

expect(
wrappedComponent.prop('intl')
).toBe(intlProvider.instance().getContext());
expect(wrapped.prop(propName)).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]);
})
describe('withRef', () => {
it('throws when true', () => {
expect(() => withIntl(Wrapped, {withRef: true})).toThrow(
'[React Intl] withRef and getWrappedInstance() are deprecated, ' +
"instead use the 'forwardRef' option and create a ref directly on the wrapped component."
);
});
});

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

rendered = mountWithProvider(<Injected />);
const wrapper = rendered.find(Injected);
describe('forwardRef', () => {
it("doesn't forward the ref when forwardRef is `false`", () => {
const Injected = withIntl(Wrapped);
const wrapperRef = React.createRef();

expect(() => wrapper.instance().getWrappedInstance()).toThrow(
'[React Intl] To access the wrapped instance, the `{withRef: true}` option must be set when calling: `withIntl()`'
);
});
rendered = mountWithProvider(<Injected ref={wrapperRef} />);
const wrapper = rendered.find(Injected);

it('does not throw when `true` getWrappedInstance() is called', () => {
Wrapped = class extends React.Component {
render () {
return null
}
}
expect(wrapperRef.current).toBe(wrapper.instance());
});

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 842e03e

Please sign in to comment.