Skip to content

Commit

Permalink
fix: avoid wrapping components with injectIntl (#1413), fixes #1412
Browse files Browse the repository at this point in the history
* refactor: avoid wrapping components with injectIntl

* test: adjust message matchers for node@12 native APIs
  • Loading branch information
emmenko authored and longlho committed Aug 13, 2019
1 parent 2d7cf7b commit ce560e7
Show file tree
Hide file tree
Showing 20 changed files with 363 additions and 432 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ lib/
locale-data/
node_modules/
src/en.js
examples/*/build

# IntelliJ
.idea/
Expand Down
2 changes: 1 addition & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Specifically, the built-in API is used to format dates/times and numbers in Reac
- [`Intl.PluralRules`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/PluralRules)
- [`Intl.RelativeTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RelativeTimeFormat)

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.
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.

## FormatJS Internationalization Formatters

Expand Down
45 changes: 27 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@
"module": "./lib/index.js",
"types": "./dist/index.d.ts",
"dependencies": {
"@formatjs/intl-relativetimeformat": "^2.6.2",
"@formatjs/intl-relativetimeformat": "^2.6.3",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/invariant": "^2.2.30",
"@types/react": "^16.0.0",
"hoist-non-react-statics": "^3.3.0",
"intl-format-cache": "^4.1.9",
"intl-locales-supported": "^1.4.4",
"intl-messageformat": "^6.1.3",
"intl-messageformat-parser": "^3.0.6",
"intl-format-cache": "^4.1.10",
"intl-locales-supported": "^1.4.5",
"intl-messageformat": "^6.1.4",
"intl-messageformat-parser": "^3.0.7",
"invariant": "^2.1.1",
"react": "^16.3.0",
"shallow-equal": "^1.1.0"
Expand Down Expand Up @@ -112,7 +112,7 @@
"release": "standard-version",
"test:all": "npm run lint && npm run format && npm run test",
"test:perf": "cross-env NODE_ENV=production babel-node test/perf",
"test:watch": "jest --watch",
"test:watch": "cross-env NODE_ICU_DATA=./node_modules/full-icu jest --watch",
"test": "cross-env NODE_ICU_DATA=./node_modules/full-icu jest --coverage --verbose",
"toc": "node scripts/toc"
},
Expand Down
80 changes: 45 additions & 35 deletions src/components/html-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,59 @@
*/

import * as React from 'react';

import {PrimitiveType} from 'intl-messageformat';
import {BaseFormattedMessage} from './message';
import injectIntl from './injectIntl';
import FormattedMessage from './message';
import {Context} from './injectIntl';
import {invariantIntlContext} from '../utils';

export class BaseFormattedHTMLMessage extends BaseFormattedMessage<
class FormattedHTMLMessage extends FormattedMessage<
Record<string, PrimitiveType>
> {
static displayName = 'FormattedHTMLMessage';
static defaultProps = {
...BaseFormattedMessage.defaultProps,
...FormattedMessage.defaultProps,
tagName: 'span' as 'span',
};
render() {
const {formatHTMLMessage, textComponent: Text} = this.props.intl;

const {
id,
description,
defaultMessage,
values: rawValues,
// This is bc of TS3.3 doesn't recognize `defaultProps`
tagName: Component = Text || 'span',
children,
} = this.props;

let descriptor = {id, description, defaultMessage};
let formattedHTMLMessage = formatHTMLMessage(descriptor, rawValues);

if (typeof children === 'function') {
return children(formattedHTMLMessage);
}

// Since the message presumably has HTML in it, we need to set
// `innerHTML` in order for it to be rendered and not escaped by React.
// To be safe, all string prop values were escaped when formatting the
// message. It is assumed that the message is not UGC, and came from the
// developer making it more like a template.
//
// Note: There's a perf impact of using this component since there's no
// way for React to do its virtual DOM diffing.
const html = {__html: formattedHTMLMessage};
return <Component dangerouslySetInnerHTML={html} />;
return (
<Context.Consumer>
{intl => {
if (!this.props.defaultMessage) {
invariantIntlContext(intl);
}

const {formatHTMLMessage, textComponent: Text} = intl;
const {
id,
description,
defaultMessage,
values: rawValues,
// This is bc of TS3.3 doesn't recognize `defaultProps`
tagName: Component = Text || 'span',
children,
} = this.props;

let descriptor = {id, description, defaultMessage};
let formattedHTMLMessage = formatHTMLMessage(descriptor, rawValues);

if (typeof children === 'function') {
return children(formattedHTMLMessage);
}

// Since the message presumably has HTML in it, we need to set
// `innerHTML` in order for it to be rendered and not escaped by React.
// To be safe, all string prop values were escaped when formatting the
// message. It is assumed that the message is not UGC, and came from the
// developer making it more like a template.
//
// Note: There's a perf impact of using this component since there's no
// way for React to do its virtual DOM diffing.
const html = {__html: formattedHTMLMessage};
return <Component dangerouslySetInnerHTML={html} />;
}}
</Context.Consumer>
);
}
}

export default injectIntl(BaseFormattedHTMLMessage, {enforceContext: false});
export default FormattedHTMLMessage;
88 changes: 45 additions & 43 deletions src/components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@
*/

import * as React from 'react';
import injectIntl, {WrappedComponentProps} from './injectIntl';
import {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat';
import {Context, WrappedComponentProps} from './injectIntl';
import {MessageDescriptor} from '../types';
const shallowEquals = require('shallow-equal/objects');

import {formatMessage as baseFormatMessage} from '../format';
import {
invariantIntlContext,
DEFAULT_INTL_CONFIG,
createFormatters,
} from '../utils';
import {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat';
const shallowEquals = require('shallow-equal/objects');

const defaultFormatMessage = (
descriptor: MessageDescriptor,
Expand Down Expand Up @@ -49,23 +48,16 @@ export interface Props<
children?(...nodes: React.ReactNodeArray): React.ReactNode;
}

export class BaseFormattedMessage<
class FormattedMessage<
V extends Record<string, any> = Record<
string,
PrimitiveType | React.ReactElement | FormatXMLElementFn
>
> extends React.Component<Props<V>> {
static displayName = 'FormattedMessage';
static defaultProps = {
values: {},
};
static displayName = 'FormattedMessage';

constructor(props: Props<V>) {
super(props);
if (!props.defaultMessage) {
invariantIntlContext(props.intl);
}
}

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

render() {
const {
formatMessage = defaultFormatMessage,
textComponent: Text = React.Fragment,
} = this.props.intl || {};
const {
id,
description,
defaultMessage,
values,
children,
tagName: Component = Text,
} = this.props;
return (
<Context.Consumer>
{intl => {
if (!this.props.defaultMessage) {
invariantIntlContext(intl);
}

const descriptor = {id, description, defaultMessage};
let nodes: string | React.ReactNodeArray = formatMessage(
descriptor,
values
);
const {
formatMessage = defaultFormatMessage,
textComponent: Text = React.Fragment,
} = intl || {};
const {
id,
description,
defaultMessage,
values,
children,
tagName: Component = Text,
} = this.props;

const descriptor = {id, description, defaultMessage};
let nodes: string | React.ReactNodeArray = formatMessage(
descriptor,
values
);

if (!Array.isArray(nodes)) {
nodes = [nodes];
}
if (!Array.isArray(nodes)) {
nodes = [nodes];
}

if (typeof children === 'function') {
return children(...nodes);
}
if (typeof children === 'function') {
return children(...nodes);
}

if (Component) {
// Needs to use `createElement()` instead of JSX, otherwise React will
// warn about a missing `key` prop with rich-text message formatting.
return React.createElement(Component, null, ...nodes);
}
return nodes;
if (Component) {
// Needs to use `createElement()` instead of JSX, otherwise React will
// warn about a missing `key` prop with rich-text message formatting.
return React.createElement(Component, null, ...nodes);
}
return nodes;
}}
</Context.Consumer>
);
}
}

export default injectIntl(BaseFormattedMessage, {enforceContext: false});
export default FormattedMessage;
Loading

0 comments on commit ce560e7

Please sign in to comment.