Skip to content

Commit ce560e7

Browse files
emmenkolonglho
authored andcommitted
fix: avoid wrapping components with injectIntl (#1413), fixes #1412
* refactor: avoid wrapping components with injectIntl * test: adjust message matchers for node@12 native APIs
1 parent 2d7cf7b commit ce560e7

20 files changed

+363
-432
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ lib/
44
locale-data/
55
node_modules/
66
src/en.js
7+
examples/*/build
78

89
# IntelliJ
910
.idea/

docs/API.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Specifically, the built-in API is used to format dates/times and numbers in Reac
4141
- [`Intl.PluralRules`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/PluralRules)
4242
- [`Intl.RelativeTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RelativeTimeFormat)
4343

44-
React Intl wraps these APIs in a consistent way making them easier to use, more performant through memoization, and gracefully fallsback when they throw errors.
44+
React Intl wraps these APIs in a consistent way making them easier to use, more performant through memoization, and gracefully falls back when they throw errors.
4545

4646
## FormatJS Internationalization Formatters
4747

package-lock.json

Lines changed: 27 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@
3434
"module": "./lib/index.js",
3535
"types": "./dist/index.d.ts",
3636
"dependencies": {
37-
"@formatjs/intl-relativetimeformat": "^2.6.2",
37+
"@formatjs/intl-relativetimeformat": "^2.6.3",
3838
"@types/hoist-non-react-statics": "^3.3.1",
3939
"@types/invariant": "^2.2.30",
4040
"@types/react": "^16.0.0",
4141
"hoist-non-react-statics": "^3.3.0",
42-
"intl-format-cache": "^4.1.9",
43-
"intl-locales-supported": "^1.4.4",
44-
"intl-messageformat": "^6.1.3",
45-
"intl-messageformat-parser": "^3.0.6",
42+
"intl-format-cache": "^4.1.10",
43+
"intl-locales-supported": "^1.4.5",
44+
"intl-messageformat": "^6.1.4",
45+
"intl-messageformat-parser": "^3.0.7",
4646
"invariant": "^2.1.1",
4747
"react": "^16.3.0",
4848
"shallow-equal": "^1.1.0"
@@ -112,7 +112,7 @@
112112
"release": "standard-version",
113113
"test:all": "npm run lint && npm run format && npm run test",
114114
"test:perf": "cross-env NODE_ENV=production babel-node test/perf",
115-
"test:watch": "jest --watch",
115+
"test:watch": "cross-env NODE_ICU_DATA=./node_modules/full-icu jest --watch",
116116
"test": "cross-env NODE_ICU_DATA=./node_modules/full-icu jest --coverage --verbose",
117117
"toc": "node scripts/toc"
118118
},

src/components/html-message.tsx

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,49 +5,59 @@
55
*/
66

77
import * as React from 'react';
8-
98
import {PrimitiveType} from 'intl-messageformat';
10-
import {BaseFormattedMessage} from './message';
11-
import injectIntl from './injectIntl';
9+
import FormattedMessage from './message';
10+
import {Context} from './injectIntl';
11+
import {invariantIntlContext} from '../utils';
1212

13-
export class BaseFormattedHTMLMessage extends BaseFormattedMessage<
13+
class FormattedHTMLMessage extends FormattedMessage<
1414
Record<string, PrimitiveType>
1515
> {
16+
static displayName = 'FormattedHTMLMessage';
1617
static defaultProps = {
17-
...BaseFormattedMessage.defaultProps,
18+
...FormattedMessage.defaultProps,
1819
tagName: 'span' as 'span',
1920
};
2021
render() {
21-
const {formatHTMLMessage, textComponent: Text} = this.props.intl;
22-
23-
const {
24-
id,
25-
description,
26-
defaultMessage,
27-
values: rawValues,
28-
// This is bc of TS3.3 doesn't recognize `defaultProps`
29-
tagName: Component = Text || 'span',
30-
children,
31-
} = this.props;
32-
33-
let descriptor = {id, description, defaultMessage};
34-
let formattedHTMLMessage = formatHTMLMessage(descriptor, rawValues);
35-
36-
if (typeof children === 'function') {
37-
return children(formattedHTMLMessage);
38-
}
39-
40-
// Since the message presumably has HTML in it, we need to set
41-
// `innerHTML` in order for it to be rendered and not escaped by React.
42-
// To be safe, all string prop values were escaped when formatting the
43-
// message. It is assumed that the message is not UGC, and came from the
44-
// developer making it more like a template.
45-
//
46-
// Note: There's a perf impact of using this component since there's no
47-
// way for React to do its virtual DOM diffing.
48-
const html = {__html: formattedHTMLMessage};
49-
return <Component dangerouslySetInnerHTML={html} />;
22+
return (
23+
<Context.Consumer>
24+
{intl => {
25+
if (!this.props.defaultMessage) {
26+
invariantIntlContext(intl);
27+
}
28+
29+
const {formatHTMLMessage, textComponent: Text} = intl;
30+
const {
31+
id,
32+
description,
33+
defaultMessage,
34+
values: rawValues,
35+
// This is bc of TS3.3 doesn't recognize `defaultProps`
36+
tagName: Component = Text || 'span',
37+
children,
38+
} = this.props;
39+
40+
let descriptor = {id, description, defaultMessage};
41+
let formattedHTMLMessage = formatHTMLMessage(descriptor, rawValues);
42+
43+
if (typeof children === 'function') {
44+
return children(formattedHTMLMessage);
45+
}
46+
47+
// Since the message presumably has HTML in it, we need to set
48+
// `innerHTML` in order for it to be rendered and not escaped by React.
49+
// To be safe, all string prop values were escaped when formatting the
50+
// message. It is assumed that the message is not UGC, and came from the
51+
// developer making it more like a template.
52+
//
53+
// Note: There's a perf impact of using this component since there's no
54+
// way for React to do its virtual DOM diffing.
55+
const html = {__html: formattedHTMLMessage};
56+
return <Component dangerouslySetInnerHTML={html} />;
57+
}}
58+
</Context.Consumer>
59+
);
5060
}
5161
}
5262

53-
export default injectIntl(BaseFormattedHTMLMessage, {enforceContext: false});
63+
export default FormattedHTMLMessage;

src/components/message.tsx

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,16 @@
55
*/
66

77
import * as React from 'react';
8-
import injectIntl, {WrappedComponentProps} from './injectIntl';
8+
import {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat';
9+
import {Context, WrappedComponentProps} from './injectIntl';
910
import {MessageDescriptor} from '../types';
10-
const shallowEquals = require('shallow-equal/objects');
11-
1211
import {formatMessage as baseFormatMessage} from '../format';
1312
import {
1413
invariantIntlContext,
1514
DEFAULT_INTL_CONFIG,
1615
createFormatters,
1716
} from '../utils';
18-
import {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat';
17+
const shallowEquals = require('shallow-equal/objects');
1918

2019
const defaultFormatMessage = (
2120
descriptor: MessageDescriptor,
@@ -49,23 +48,16 @@ export interface Props<
4948
children?(...nodes: React.ReactNodeArray): React.ReactNode;
5049
}
5150

52-
export class BaseFormattedMessage<
51+
class FormattedMessage<
5352
V extends Record<string, any> = Record<
5453
string,
5554
PrimitiveType | React.ReactElement | FormatXMLElementFn
5655
>
5756
> extends React.Component<Props<V>> {
57+
static displayName = 'FormattedMessage';
5858
static defaultProps = {
5959
values: {},
6060
};
61-
static displayName = 'FormattedMessage';
62-
63-
constructor(props: Props<V>) {
64-
super(props);
65-
if (!props.defaultMessage) {
66-
invariantIntlContext(props.intl);
67-
}
68-
}
6961

7062
shouldComponentUpdate(nextProps: Props<V>) {
7163
const {values, ...otherProps} = this.props;
@@ -77,40 +69,50 @@ export class BaseFormattedMessage<
7769
}
7870

7971
render() {
80-
const {
81-
formatMessage = defaultFormatMessage,
82-
textComponent: Text = React.Fragment,
83-
} = this.props.intl || {};
84-
const {
85-
id,
86-
description,
87-
defaultMessage,
88-
values,
89-
children,
90-
tagName: Component = Text,
91-
} = this.props;
72+
return (
73+
<Context.Consumer>
74+
{intl => {
75+
if (!this.props.defaultMessage) {
76+
invariantIntlContext(intl);
77+
}
9278

93-
const descriptor = {id, description, defaultMessage};
94-
let nodes: string | React.ReactNodeArray = formatMessage(
95-
descriptor,
96-
values
97-
);
79+
const {
80+
formatMessage = defaultFormatMessage,
81+
textComponent: Text = React.Fragment,
82+
} = intl || {};
83+
const {
84+
id,
85+
description,
86+
defaultMessage,
87+
values,
88+
children,
89+
tagName: Component = Text,
90+
} = this.props;
91+
92+
const descriptor = {id, description, defaultMessage};
93+
let nodes: string | React.ReactNodeArray = formatMessage(
94+
descriptor,
95+
values
96+
);
9897

99-
if (!Array.isArray(nodes)) {
100-
nodes = [nodes];
101-
}
98+
if (!Array.isArray(nodes)) {
99+
nodes = [nodes];
100+
}
102101

103-
if (typeof children === 'function') {
104-
return children(...nodes);
105-
}
102+
if (typeof children === 'function') {
103+
return children(...nodes);
104+
}
106105

107-
if (Component) {
108-
// Needs to use `createElement()` instead of JSX, otherwise React will
109-
// warn about a missing `key` prop with rich-text message formatting.
110-
return React.createElement(Component, null, ...nodes);
111-
}
112-
return nodes;
106+
if (Component) {
107+
// Needs to use `createElement()` instead of JSX, otherwise React will
108+
// warn about a missing `key` prop with rich-text message formatting.
109+
return React.createElement(Component, null, ...nodes);
110+
}
111+
return nodes;
112+
}}
113+
</Context.Consumer>
114+
);
113115
}
114116
}
115117

116-
export default injectIntl(BaseFormattedMessage, {enforceContext: false});
118+
export default FormattedMessage;

0 commit comments

Comments
 (0)