From 227a4442dd441d1ff377a04c70358f32cb2e287e Mon Sep 17 00:00:00 2001 From: Long Ho Date: Thu, 1 Aug 2019 22:43:13 -0400 Subject: [PATCH] feat: simplify provider inheritance, add new APIs (#1387) Also introduces `createIntl` and `RawIntlProvider` fixes #1386 fixes #1376 ## Creating intl without using Provider We've added a new API called `createIntl` that allows you to create an `IntlShape` object without using `Provider`. This allows you to format things outside of React lifecycle while reusing the same `intl` object. For example: ```tsx import {createIntl, createIntlCache, RawIntlProvider} from 'react-intl' // This is optional but highly recommended // since it prevents memory leak const cache = createIntlCache() const intl = createIntl({ locale: 'fr-FR', messages: {} }, cache) // Call imperatively intl.formatNumber(20) // Pass it to IntlProvider {foo} ``` This is especially beneficial in SSR where you can reuse the same `intl` object across requests. --- docs/API.md | 25 +++ docs/Components.md | 22 ++- docs/Testing-with-React-Intl.md | 8 +- docs/Upgrade-Guide.md | 29 ++- src/components/message.tsx | 26 +-- src/components/provider.tsx | 252 ++++++++++++-------------- src/core.ts | 4 +- src/index.ts | 3 +- src/test-utils.ts | 13 -- src/types.ts | 8 + src/utils.ts | 36 +++- test/unit/components/date.tsx | 9 +- test/unit/components/html-message.tsx | 4 +- test/unit/components/message.tsx | 53 +++--- test/unit/components/number.tsx | 6 +- test/unit/components/plural.tsx | 4 +- test/unit/components/provider.tsx | 45 ----- test/unit/components/relative.tsx | 8 +- test/unit/components/time.tsx | 6 +- test/unit/components/useIntl.tsx | 5 +- test/unit/components/withIntl.tsx | 12 +- test/unit/testUtils.tsx | 6 +- 22 files changed, 295 insertions(+), 289 deletions(-) delete mode 100644 src/test-utils.ts diff --git a/docs/API.md b/docs/API.md index 789f6da085..e079829a9a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -12,6 +12,7 @@ There are a few API layers that React Intl provides and is built on. When using - [`useIntl` hook (currently available in 3.0.0 beta)](#useintl-hook-currently-available-in-300-beta) - [`injectIntl` HOC](#injectintl-hoc) - [`IntlShape`](#intlshape) + - [`createIntl`](#createintl) - [Date Formatting APIs](#date-formatting-apis) - [`formatDate`](#formatdate) - [`formatTime`](#formattime) @@ -86,6 +87,7 @@ React Intl provides: 1. [`useIntl` hook](#useintl-hook): to _hook_ the imperative formatting API into a React function component (with React version >= 16.8). 2. [`injectIntl` HOC](#injectintl-hoc): to _inject_ the imperative formatting API into a React class or function component via its `props`. +3. [`createIntl`](#createintl): to create `IntlShape` object outside of React lifecycle. These should be used when your React component needs to format data to a string value where a React element is not suitable; e.g., a `title` or `aria` attribute, or for side-effect in `componentDidMount`. @@ -198,6 +200,29 @@ The definition above shows what the `props.intl` object will look like that's in - **`IntlConfig`:** The intl metadata passed as props into the parent ``. - **`IntlFormatters`:** The imperative formatting API described below. +#### `createIntl` + +This allows you to create an `IntlShape` object without using `Provider`. This allows you to format things outside of React lifecycle while reusing the same `intl` object. For example: + +```tsx +import {createIntl, createIntlCache, RawIntlProvider} from 'react-intl' + +// This is optional but highly recommended +// since it prevents memory leak +const cache = createIntlCache() + +const intl = createIntl({ + locale: 'fr-FR', + messages: {} +}, cache) + +// Call imperatively +intl.formatNumber(20) + +// Pass it to IntlProvider +{foo} +``` + ### Date Formatting APIs React Intl provides three functions to format dates: diff --git a/docs/Components.md b/docs/Components.md index ce4396bd1f..87de843804 100644 --- a/docs/Components.md +++ b/docs/Components.md @@ -7,7 +7,7 @@ React Intl has a set of React components that provide a declarative way to setup - [Why Components?](#why-components) - [Intl Provider Component](#intl-provider-component) - [`IntlProvider`](#intlprovider) - - [Multiple Intl Contexts](#multiple-intl-contexts) + - [`RawIntlProvider`](#rawintlprovider) - [Dynamic Language Selection](#dynamic-language-selection) - [Date Formatting Components](#date-formatting-components) - [`FormattedDate`](#formatteddate) @@ -109,9 +109,25 @@ Assuming `navigator.language` is `"fr"`:
mardi 5 avril 2016
``` -#### Multiple Intl Contexts +### `RawIntlProvider` -Nested `` components can be used to provide a different, or modified i18n context to a subtree of the app. In these cases, the nested `` will inherit from its nearest ancestor ``. A nested strategy can be employed to provide a subset of translations to a subtree. See: [Nested Example app](https://github.com/formatjs/react-intl/tree/master/examples/nested) +This is the underlying `React.Context.Provider` object that `IntlProvider` use. It can be used in conjunction with `createIntl`: + +```tsx +import {createIntl, createIntlCache, RawIntlProvider} from 'react-intl' + +// This is optional but highly recommended +// since it prevents memory leak +const cache = createIntlCache() + +const intl = createIntl({ + locale: 'fr-FR', + messages: {} +}, cache) + +// Pass it to IntlProvider +{foo} +``` #### Dynamic Language Selection diff --git a/docs/Testing-with-React-Intl.md b/docs/Testing-with-React-Intl.md index 31c6acf0d7..f66600b7a9 100644 --- a/docs/Testing-with-React-Intl.md +++ b/docs/Testing-with-React-Intl.md @@ -136,7 +136,7 @@ import expect from 'expect'; import expectJSX from 'expect-jsx'; import React from 'react'; import {createRenderer} from 'react-addons-test-utils'; -import {IntlProvider, FormattedRelative, generateIntlContext} from 'react-intl'; +import {IntlProvider, FormattedRelative, createIntl} from 'react-intl'; import RelativeDate from '../relative-date'; expect.extend(expectJSX); @@ -146,7 +146,7 @@ describe('', function() { const renderer = createRenderer(); const date = new Date(); - const intl = generateIntlContext({ + const intl = createIntl({ locale: 'en', defaultLocale: 'en', }); @@ -252,13 +252,13 @@ Testing with Enzyme works in a similar fashion as written above. Your `mount()`e */ import React from 'react'; -import {IntlProvider, generateIntlContext} from 'react-intl'; +import {IntlProvider, createIntl} from 'react-intl'; import {mount, shallow} from 'enzyme'; // You can pass your messages to the IntlProvider. Optional: remove if unneeded. const messages = require('../locales/en'); // en.json -const intl = generateIntlContext({ +const intl = createIntl({ locale: 'en', defaultLocale: 'en', messages, diff --git a/docs/Upgrade-Guide.md b/docs/Upgrade-Guide.md index 4798898a91..4588304f6e 100644 --- a/docs/Upgrade-Guide.md +++ b/docs/Upgrade-Guide.md @@ -16,6 +16,7 @@ - [Jest](#jest) - [webpack babel-loader](#webpack-babel-loader) - [Apostrophe Escape](#apostrophe-escape) +- [Creating intl without using Provider](#creating-intl-without-using-provider) @@ -32,7 +33,8 @@ - `FormattedRelative` has been renamed to `FormattedRelativeTime` and its API has changed significantly. See [FormattedRelativeTime](#formattedrelativetime) for more details. - `formatRelative` has been renamed to `formatRelativeTime` and its API has changed significantly. See [FormattedRelativeTime](#formattedrelativetime) for more details. -- Escape character has been changed to apostrophe (`'`). See [Apostrophe Escape](#apostrophe-escape) for more details +- Escape character has been changed to apostrophe (`'`). See [Apostrophe Escape](#apostrophe-escape) for more details. +- `IntlProvider` no longer inherits from upstream `IntlProvider`. ## Use React 16.3 and upwards @@ -350,3 +352,28 @@ Previously while we were using ICU message format syntax, our escape char was ba ``` We highly recommend reading the spec to learn more about how quote/escaping works [here](http://userguide.icu-project.org/formatparse/messages) under **Quoting/Escaping** section. + +## Creating intl without using Provider + +We've added a new API called `createIntl` that allows you to create an `IntlShape` object without using `Provider`. This allows you to format things outside of React lifecycle while reusing the same `intl` object. For example: + +```tsx +import {createIntl, createIntlCache, RawIntlProvider} from 'react-intl' + +// This is optional but highly recommended +// since it prevents memory leak +const cache = createIntlCache() + +const intl = createIntl({ + locale: 'fr-FR', + messages: {} +}, cache) + +// Call imperatively +intl.formatNumber(20) + +// Pass it to IntlProvider +{foo} +``` + +This is especially beneficial in SSR where you can reuse the same `intl` object across requests. diff --git a/src/components/message.tsx b/src/components/message.tsx index 5f86e742b5..ade05df392 100644 --- a/src/components/message.tsx +++ b/src/components/message.tsx @@ -13,7 +13,7 @@ import {formatMessage as baseFormatMessage} from '../format'; import { invariantIntlContext, DEFAULT_INTL_CONFIG, - createDefaultFormatters, + createFormatters, } from '../utils'; import {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat/core'; @@ -35,7 +35,7 @@ const defaultFormatMessage = ( ...DEFAULT_INTL_CONFIG, locale: 'en', }, - createDefaultFormatters(), + createFormatters(), descriptor, values as any ); @@ -67,22 +67,12 @@ export class BaseFormattedMessage< } shouldComponentUpdate(nextProps: Props) { - const {values} = this.props; - const {values: nextValues} = nextProps; - - if (!shallowEquals(nextValues, values)) { - return true; - } - - // Since `values` has already been checked, we know they're not - // different, so the current `values` are carried over so the shallow - // equals comparison on the other props isn't affected by the `values`. - let nextPropsToCheck = { - ...nextProps, - values, - }; - - return !shallowEquals(this.props, nextPropsToCheck); + const {values, ...otherProps} = this.props; + const {values: nextValues, ...nextOtherProps} = nextProps; + return ( + !shallowEquals(nextValues, values) || + !shallowEquals(otherProps, nextOtherProps) + ); } render() { diff --git a/src/components/provider.tsx b/src/components/provider.tsx index d88d307f8d..b3c79ab42c 100644 --- a/src/components/provider.tsx +++ b/src/components/provider.tsx @@ -5,175 +5,153 @@ */ import * as React from 'react'; -import withIntl, {Provider, WrappedComponentProps} from './injectIntl'; +import {Provider} from './injectIntl'; import { createError, - filterProps, DEFAULT_INTL_CONFIG, - createDefaultFormatters, + createFormatters, + invariantIntlContext, + createIntlCache, } from '../utils'; +import {IntlConfig, IntlShape, Omit, IntlCache} from '../types'; import { - IntlConfig, - IntlShape, - IntlFormatters, - Omit, - Formatters, -} from '../types'; -import {formatters} from '../format'; + formatNumber, + formatRelativeTime, + formatDate, + formatTime, + formatPlural, + formatHTMLMessage, + formatMessage, +} from '../format'; import areIntlLocalesSupported from 'intl-locales-supported'; const shallowEquals = require('shallow-equal/objects'); -const intlConfigPropNames: Array = [ - 'locale', - 'timeZone', - 'formats', - 'messages', - 'textComponent', - - 'defaultLocale', - 'defaultFormats', +interface State { + /** + * Explicit intl cache to prevent memory leaks + */ + cache: IntlCache; + /** + * Intl object we created + */ + intl?: IntlShape; + /** + * list of memoized props we care about. + * This is important since creating intl is + * very expensive + */ + prevProps: OptionalIntlConfig; +} - 'onError', -]; +export type OptionalIntlConfig = Omit< + IntlConfig, + keyof typeof DEFAULT_INTL_CONFIG +> & + Partial; -function getConfig(filteredProps: OptionalIntlConfig): IntlConfig { - let config: IntlConfig = { - ...DEFAULT_INTL_CONFIG, - ...filteredProps, +export default class IntlProvider extends React.PureComponent< + OptionalIntlConfig, + State +> { + static displayName: string = 'IntlProvider'; + static defaultProps = DEFAULT_INTL_CONFIG; + private cache: IntlCache = createIntlCache(); + state: State = { + cache: this.cache, + intl: undefined, + prevProps: { + locale: this.props.locale, + }, }; - // Apply default props. This must be applied last after the props have - // been resolved and inherited from any in the ancestry. - // This matches how React resolves `defaultProps`. - for (const propName in DEFAULT_INTL_CONFIG) { - if (config[propName as 'timeZone'] === undefined) { - config[propName as 'timeZone'] = - DEFAULT_INTL_CONFIG[propName as 'timeZone']; + static getDerivedStateFromProps( + props: OptionalIntlConfig, + {prevProps, cache}: State + ) { + const { + locale, + timeZone, + formats, + textComponent, + messages, + defaultLocale, + defaultFormats, + onError, + } = props; + const filteredProps = { + locale, + timeZone, + formats, + textComponent, + messages, + defaultLocale, + defaultFormats, + onError, + }; + if (!shallowEquals(prevProps, filteredProps)) { + return { + intl: createIntl(props, cache), + prevProps: filteredProps, + }; } + return null; + } + + render() { + invariantIntlContext(this.state); + return {this.props.children}; } +} - if (!config.locale || !areIntlLocalesSupported(config.locale)) { - const {locale, defaultLocale, defaultFormats, onError} = config; - if (typeof onError === 'function') +/** + * Create intl object + * @param config intl config + * @param cache cache for formatter instances to prevent memory leak + */ +export function createIntl( + config: OptionalIntlConfig, + cache?: IntlCache +): IntlShape { + const formatters = createFormatters(cache); + const resolvedConfig = {...DEFAULT_INTL_CONFIG, ...config}; + if ( + !resolvedConfig.locale || + !areIntlLocalesSupported(resolvedConfig.locale) + ) { + const {locale, defaultLocale, onError} = resolvedConfig; + if (typeof onError === 'function') { onError( createError( `Missing locale data for locale: "${locale}". ` + `Using default locale: "${defaultLocale}" as fallback.` ) ); + } // Since there's no registered locale data for `locale`, this will // fallback to the `defaultLocale` to make sure things can render. // The `messages` are overridden to the `defaultProps` empty object // to maintain referential equality across re-renders. It's assumed // each contains a `defaultMessage` prop. - config = { - ...config, - locale: defaultLocale, - formats: defaultFormats, - messages: DEFAULT_INTL_CONFIG.messages, - }; + resolvedConfig.locale = resolvedConfig.defaultLocale || 'en'; } - - return config; -} - -// Public primarily for testing -export function getBoundFormatFns( - config: IntlConfig, - formatterFns: Formatters -): IntlFormatters { return { - formatNumber: formatters.formatNumber.bind(undefined, config, formatterFns), - formatRelativeTime: formatters.formatRelativeTime.bind( - undefined, - config, - formatterFns - ), - formatDate: formatters.formatDate.bind(undefined, config, formatterFns), - formatTime: formatters.formatTime.bind(undefined, config, formatterFns), - formatPlural: formatters.formatPlural.bind(undefined, config, formatterFns), - formatMessage: formatters.formatMessage.bind( + ...resolvedConfig, + formatters, + formatNumber: formatNumber.bind(undefined, resolvedConfig, formatters), + formatRelativeTime: formatRelativeTime.bind( undefined, - config, - formatterFns + resolvedConfig, + formatters ), - formatHTMLMessage: formatters.formatHTMLMessage.bind( + formatDate: formatDate.bind(undefined, resolvedConfig, formatters), + formatTime: formatTime.bind(undefined, resolvedConfig, formatters), + formatPlural: formatPlural.bind(undefined, resolvedConfig, formatters), + formatMessage: formatMessage.bind(undefined, resolvedConfig, formatters), + formatHTMLMessage: formatHTMLMessage.bind( undefined, - config, - formatterFns + resolvedConfig, + formatters ), }; } - -interface State { - context: IntlShape; - filteredProps?: IntlConfig; -} - -export type OptionalIntlConfig = Omit< - IntlConfig, - keyof typeof DEFAULT_INTL_CONFIG -> & - Partial; - -export type Props = WrappedComponentProps & OptionalIntlConfig; - -class IntlProvider extends React.PureComponent { - constructor(props: Props) { - super(props); - - const {intl: intlContext} = props; - - // Creating `Intl*` formatters is expensive. If there's a parent - // ``, then its formatters will be used. Otherwise, this - // memoize the `Intl*` constructors and cache them for the lifecycle of - // this IntlProvider instance. - const {formatters = createDefaultFormatters()} = intlContext || {}; - - this.state = { - context: { - ...intlContext!, - formatters, - }, - }; - } - - static getDerivedStateFromProps(nextProps: Props, prevState: State) { - const {intl: intlContext} = nextProps; - - // Build a whitelisted config object from `props`, defaults, and - // `props.intl`, if an exists in the ancestry. - const filteredProps = filterProps( - nextProps, - intlConfigPropNames, - intlContext || {} - ); - - if (!shallowEquals(filteredProps, prevState.filteredProps)) { - const config = getConfig(filteredProps); - const boundFormatFns = getBoundFormatFns(config, { - ...prevState.context.formatters, - }); - - return { - filteredProps, - context: { - ...prevState.context, - ...config, - ...boundFormatFns, - }, - }; - } - - return null; - } - - render() { - return ( - {this.props.children} - ); - } -} - -export default withIntl(IntlProvider, {enforceContext: false}); // to be able to inherit values from parent providers diff --git a/src/core.ts b/src/core.ts index 828d32fe50..3d00e51edf 100644 --- a/src/core.ts +++ b/src/core.ts @@ -9,12 +9,13 @@ export {default as defineMessages} from './define-messages'; import createFormattedComponent from './components/createFormattedComponent'; export { default as injectIntl, + Provider as RawIntlProvider, Context as IntlContext, WithIntlProps, WrappedComponentProps, } from './components/injectIntl'; export {default as useIntl} from './components/useIntl'; -export {default as IntlProvider} from './components/provider'; +export {default as IntlProvider, createIntl} from './components/provider'; export const {Component: FormattedDate} = createFormattedComponent( 'formatDate' ); @@ -28,4 +29,3 @@ export {default as FormattedRelativeTime} from './components/relative'; export {default as FormattedPlural} from './components/plural'; export {default as FormattedMessage} from './components/message'; export {default as FormattedHTMLMessage} from './components/html-message'; -export {generateIntlContext} from './test-utils'; diff --git a/src/index.ts b/src/index.ts index c96d05174b..afdaf8ef2c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import {parse} from 'intl-messageformat-parser'; import {IntlMessageFormat} from 'intl-messageformat/core'; IntlMessageFormat.__parse = parse; export { + createIntl, defineMessages, FormattedDate, FormattedHTMLMessage, @@ -17,10 +18,10 @@ export { FormattedPlural, FormattedRelativeTime, FormattedTime, - generateIntlContext, injectIntl, IntlContext, IntlProvider, + RawIntlProvider, useIntl, WithIntlProps, WrappedComponentProps, diff --git a/src/test-utils.ts b/src/test-utils.ts deleted file mode 100644 index b6a6cca7f5..0000000000 --- a/src/test-utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {createDefaultFormatters, DEFAULT_INTL_CONFIG} from './utils'; -import {OptionalIntlConfig, getBoundFormatFns} from './components/provider'; -import {IntlShape} from './types'; - -export function generateIntlContext(config: OptionalIntlConfig): IntlShape { - const formatters = createDefaultFormatters(); - const resolvedConfig = {...DEFAULT_INTL_CONFIG, ...config}; - return { - ...resolvedConfig, - formatters, - ...getBoundFormatFns(resolvedConfig, formatters), - }; -} diff --git a/src/types.ts b/src/types.ts index 5d93a13c2e..121d68b4e1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -108,6 +108,14 @@ export interface IntlShape extends IntlConfig, IntlFormatters { formatters: Formatters; } +export interface IntlCache { + dateTime: Record; + number: Record; + message: Record; + relativeTime: Record; + pluralRules: Record; +} + export interface MessageDescriptor { id: string; description?: string | object; diff --git a/src/utils.ts b/src/utils.ts index 08d6cdf0a6..f4e646342a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,7 +9,7 @@ This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of React's source tree. */ -import {IntlConfig} from './types'; +import {IntlConfig, IntlCache} from './types'; import * as React from 'react'; import {IntlMessageFormat} from 'intl-messageformat/core'; import memoizeIntlConstructor from 'intl-format-cache'; @@ -82,8 +82,6 @@ export function defaultErrorHandler(error: string) { } } -// These are not a static property on the `IntlProvider` class so the intl -// config values can be inherited from an ancestor. export const DEFAULT_INTL_CONFIG: Pick< IntlConfig, | 'formats' @@ -105,12 +103,32 @@ export const DEFAULT_INTL_CONFIG: Pick< onError: defaultErrorHandler, }; -export function createDefaultFormatters() { +export function createIntlCache(): IntlCache { return { - getDateTimeFormat: memoizeIntlConstructor(Intl.DateTimeFormat), - getNumberFormat: memoizeIntlConstructor(Intl.NumberFormat), - getMessageFormat: memoizeIntlConstructor(IntlMessageFormat), - getRelativeTimeFormat: memoizeIntlConstructor(Intl.RelativeTimeFormat), - getPluralRules: memoizeIntlConstructor(Intl.PluralRules), + dateTime: {}, + number: {}, + message: {}, + relativeTime: {}, + pluralRules: {}, + }; +} + +/** + * Create intl formatters and populate cache + * @param cache explicit cache to prevent leaking memory + */ +export function createFormatters(cache: IntlCache = createIntlCache()) { + return { + getDateTimeFormat: memoizeIntlConstructor( + Intl.DateTimeFormat, + cache.dateTime + ), + getNumberFormat: memoizeIntlConstructor(Intl.NumberFormat, cache.number), + getMessageFormat: memoizeIntlConstructor(IntlMessageFormat, cache.message), + getRelativeTimeFormat: memoizeIntlConstructor( + Intl.RelativeTimeFormat, + cache.relativeTime + ), + getPluralRules: memoizeIntlConstructor(Intl.PluralRules, cache.pluralRules), }; } diff --git a/test/unit/components/date.tsx b/test/unit/components/date.tsx index 5beb088b9e..2faf8037e8 100644 --- a/test/unit/components/date.tsx +++ b/test/unit/components/date.tsx @@ -2,17 +2,18 @@ import * as React from 'react'; import {mount} from 'enzyme'; import {FormattedDate} from '../../../src'; import {mountFormattedComponentWithProvider} from '../testUtils'; -import {generateIntlContext} from '../../../src/test-utils'; +import {createIntl} from '../../../src/components/provider'; +import {IntlShape} from '../../../src'; const mountWithProvider = mountFormattedComponentWithProvider(FormattedDate); describe('', () => { let consoleError; - let intl; + let intl: IntlShape; beforeEach(() => { consoleError = jest.spyOn(console, 'error'); - intl = generateIntlContext({ + intl = createIntl({ locale: 'en', }); }); @@ -81,7 +82,7 @@ describe('', () => { }); it('accepts `format` prop', () => { - intl = generateIntlContext({ + intl = createIntl({ locale: 'en', formats: { date: { diff --git a/test/unit/components/html-message.tsx b/test/unit/components/html-message.tsx index f5f1829a32..6af97192c2 100644 --- a/test/unit/components/html-message.tsx +++ b/test/unit/components/html-message.tsx @@ -4,7 +4,7 @@ import FormattedHTMLMessage, { BaseFormattedHTMLMessage, } from '../../../src/components/html-message'; import {BaseFormattedMessage} from '../../../src/components/message'; -import {generateIntlContext} from '../../../src/test-utils'; +import {createIntl} from '../../../src/components/provider'; const mountWithProvider = mountFormattedComponentWithProvider( FormattedHTMLMessage @@ -16,7 +16,7 @@ describe('', () => { beforeEach(() => { consoleError = jest.spyOn(console, 'error'); - intl = generateIntlContext({ + intl = createIntl({ locale: 'en', defaultLocale: 'en-US', textComponent: 'span', diff --git a/test/unit/components/message.tsx b/test/unit/components/message.tsx index 90a0a13212..be00c1c1d6 100644 --- a/test/unit/components/message.tsx +++ b/test/unit/components/message.tsx @@ -1,21 +1,23 @@ import * as React from 'react'; import FormattedMessage from '../../../src/components/message'; -import {generateIntlContext} from '../../../src/test-utils'; +import {Props, createIntl} from '../../../src/components/provider'; import {mountFormattedComponentWithProvider} from '../testUtils'; import {mount} from 'enzyme'; +import {IntlShape} from '../../../src'; const mountWithProvider = mountFormattedComponentWithProvider(FormattedMessage); describe('', () => { let consoleError; - let intl; + let providerProps: Props; + let intl: IntlShape; beforeEach(() => { - intl = generateIntlContext({ + providerProps = { locale: 'en', defaultLocale: 'en', - }); - + }; + intl = createIntl(providerProps); consoleError = jest.spyOn(console, 'error'); }); @@ -48,8 +50,7 @@ describe('', () => { id: 'hello', defaultMessage: 'Hello, World!', }; - - const rendered = mountWithProvider(descriptor, intl); + const rendered = mountWithProvider(descriptor, providerProps); expect(rendered.text()).toBe(intl.formatMessage(descriptor)); }); @@ -60,8 +61,7 @@ describe('', () => { defaultMessage: 'Hello, {name}!', }; const values = {name: 'Jest'}; - - const rendered = mountWithProvider({...descriptor, values}, intl); + const rendered = mountWithProvider({...descriptor, values}, providerProps); expect(rendered.text()).toBe(intl.formatMessage(descriptor, values)); }); @@ -73,9 +73,10 @@ describe('', () => { }; const tagName = 'p'; - const rendered = mountWithProvider({...descriptor, tagName}, intl).find( - 'p' - ); + const rendered = mountWithProvider( + {...descriptor, tagName}, + providerProps + ).find('p'); expect(rendered.type()).toBe(tagName); }); @@ -87,9 +88,10 @@ describe('', () => { }; const H1 = ({children}) =>

{children}

; - const rendered = mountWithProvider({...descriptor, tagName: H1}, intl).find( - H1 - ); + const rendered = mountWithProvider( + {...descriptor, tagName: H1}, + providerProps + ).find(H1); expect(rendered.type()).toBe(H1); expect(rendered.text()).toBe(intl.formatMessage(descriptor)); @@ -103,7 +105,10 @@ describe('', () => { const spy = jest.fn().mockImplementation(() =>

Jest

); - const rendered = mountWithProvider({...descriptor, children: spy}, intl); + const rendered = mountWithProvider( + {...descriptor, children: spy}, + providerProps + ); expect(spy).toHaveBeenCalledTimes(2); expect(spy.mock.calls[0]).toEqual([intl.formatMessage(descriptor)]); @@ -121,7 +126,7 @@ describe('', () => { name: Jest, }, }, - intl + providerProps ); const nameNode = rendered.find('b'); @@ -138,7 +143,7 @@ describe('', () => { b: (name: string) => {name}, }, }, - intl + providerProps ); const nameNode = rendered.find('b'); @@ -155,7 +160,7 @@ describe('', () => { name: Jest, }, }, - intl + providerProps ); const nameNode = rendered.find('b'); @@ -173,7 +178,7 @@ describe('', () => { }, children: (...chunks) => {chunks}, }, - intl + providerProps ); const nameNode = rendered.find('b'); @@ -198,16 +203,18 @@ describe('', () => { values, children: spy, }, - intl + providerProps ); + expect(spy).toHaveBeenCalled(); + spy.mockClear(); injectIntlContext.setProps({ ...descriptor, values: { ...values, // create new object instance with same values to test shallow equality check }, }); - expect(spy).toHaveBeenCalledTimes(2); // expect only 1 render as the value object instance changed but not its values + expect(spy).not.toHaveBeenCalled(); injectIntlContext.setProps({ ...descriptor, @@ -215,6 +222,6 @@ describe('', () => { name: 'Enzyme', }, }); - expect(spy).toHaveBeenCalledTimes(4); // expect a rerender after having changed the name + expect(spy).toHaveBeenCalled(); }); }); diff --git a/test/unit/components/number.tsx b/test/unit/components/number.tsx index 2ce2516e9a..bd22ceb8cd 100644 --- a/test/unit/components/number.tsx +++ b/test/unit/components/number.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import {mount} from 'enzyme'; import {FormattedNumber} from '../../../src'; -import {generateIntlContext} from '../../../src/test-utils'; +import {createIntl} from '../../../src/components/provider'; import {mountFormattedComponentWithProvider} from '../testUtils'; const mountWithProvider = mountFormattedComponentWithProvider(FormattedNumber); @@ -11,7 +11,7 @@ describe('', () => { beforeEach(() => { consoleError = jest.spyOn(console, 'error'); - intl = generateIntlContext({ + intl = createIntl({ locale: 'en', }); }); @@ -72,7 +72,7 @@ describe('', () => { }); it('accepts `format` prop', () => { - intl = generateIntlContext({ + intl = createIntl({ locale: 'en', formats: { number: { diff --git a/test/unit/components/plural.tsx b/test/unit/components/plural.tsx index a1f98762cd..c940fd1135 100644 --- a/test/unit/components/plural.tsx +++ b/test/unit/components/plural.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import {mount} from 'enzyme'; import FormattedPlural from '../../../src/components/plural'; import {mountFormattedComponentWithProvider} from '../testUtils'; -import {generateIntlContext} from '../../../src/test-utils'; +import {createIntl} from '../../../src/components/provider'; const mountWithProvider = mountFormattedComponentWithProvider(FormattedPlural); @@ -12,7 +12,7 @@ describe('', () => { beforeEach(() => { consoleError = jest.spyOn(console, 'error'); - intl = generateIntlContext({ + intl = createIntl({ locale: 'en', }); }); diff --git a/test/unit/components/provider.tsx b/test/unit/components/provider.tsx index edab867a18..e843c67c69 100644 --- a/test/unit/components/provider.tsx +++ b/test/unit/components/provider.tsx @@ -184,51 +184,6 @@ describe('', () => { expect(intl.formatDate(date, {format: 'year-only'})).toBe(df.format(date)); }); - it('inherits from an ancestor', () => { - const props: WithIntlProps = { - locale: 'en', - timeZone: 'UTC', - formats: { - date: { - 'year-only': { - year: 'numeric', - }, - }, - }, - messages: { - hello: 'Hello, World!', - }, - textComponent: 'span', - - defaultLocale: 'fr', - defaultFormats: { - date: { - 'year-only': { - year: 'numeric', - }, - }, - }, - - onError: consoleError, - }; - - const intl = mount( - - - - - - ) - .find(Child) - .prop('intl'); - - expect(consoleError).toHaveBeenCalledTimes(0); - - INTL_CONFIG_PROP_NAMES.forEach(propName => { - expect(intl[propName]).toBe(props[propName]); - }); - }); - it('shadows inherited intl config props from an ancestor', () => { const props = { locale: 'en', diff --git a/test/unit/components/relative.tsx b/test/unit/components/relative.tsx index 7d0946c858..68d9f34a9a 100644 --- a/test/unit/components/relative.tsx +++ b/test/unit/components/relative.tsx @@ -6,7 +6,7 @@ import {mount} from 'enzyme'; import FormattedRelativeTime, { BaseFormattedRelativeTime, } from '../../../src/components/relative'; -import {generateIntlContext} from '../../../src/test-utils'; +import {createIntl} from '../../../src/components/provider'; import {IntlShape} from '../../../src/types'; import {mountFormattedComponentWithProvider} from '../testUtils'; @@ -20,7 +20,7 @@ describe('', () => { beforeEach(() => { consoleError = jest.spyOn(console, 'error'); - intl = generateIntlContext({ + intl = createIntl({ locale: 'en', }); }); @@ -49,7 +49,7 @@ describe('', () => { }); it('should re-render when context changes', () => { - const otherIntl = generateIntlContext({ + const otherIntl = createIntl({ locale: 'en-US', }); const spy = jest.fn().mockImplementation(() => null); @@ -88,7 +88,7 @@ describe('', () => { it('accepts `format` prop', () => { const format = 'seconds'; - intl = generateIntlContext({ + intl = createIntl({ locale: 'en', formats: { relative: { diff --git a/test/unit/components/time.tsx b/test/unit/components/time.tsx index b6666e07cc..3db299d219 100644 --- a/test/unit/components/time.tsx +++ b/test/unit/components/time.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import {mount} from 'enzyme'; import {FormattedTime} from '../../../src'; import {mountFormattedComponentWithProvider} from '../testUtils'; -import {generateIntlContext} from '../../../src/test-utils'; +import {createIntl} from '../../../src/components/provider'; const mountWithProvider = mountFormattedComponentWithProvider(FormattedTime); @@ -12,7 +12,7 @@ describe('', () => { beforeEach(() => { consoleError = jest.spyOn(console, 'error'); - intl = generateIntlContext({ + intl = createIntl({ locale: 'en', }); }); @@ -83,7 +83,7 @@ describe('', () => { }); it('accepts `format` prop', () => { - intl = generateIntlContext({ + intl = createIntl({ locale: 'en', formats: { time: { diff --git a/test/unit/components/useIntl.tsx b/test/unit/components/useIntl.tsx index 20f192d839..6b90994229 100644 --- a/test/unit/components/useIntl.tsx +++ b/test/unit/components/useIntl.tsx @@ -27,10 +27,7 @@ describe('useIntl() hook', () => { ); - const intl = rendered - .find(IntlProvider) - .childAt(0) - .state('context'); + const intl = rendered.state('intl'); expect(spy).toHaveBeenCalledWith(intl); }); }); diff --git a/test/unit/components/withIntl.tsx b/test/unit/components/withIntl.tsx index aa0233ac58..9544ff0414 100644 --- a/test/unit/components/withIntl.tsx +++ b/test/unit/components/withIntl.tsx @@ -58,9 +58,9 @@ describe('injectIntl()', () => { rendered = mountWithProvider(); const wrappedComponent = rendered.find(Wrapped); // React 16 renders different in the wrapper - const intlProvider = rendered.find(IntlProvider).childAt(0); + const intl = rendered.state('intl'); - expect(wrappedComponent.prop('intl')).toBe(intlProvider.state('context')); + expect(wrappedComponent.prop('intl')).toBe(intl); }); it('propagates all props to ', () => { @@ -87,9 +87,9 @@ describe('injectIntl()', () => { rendered = mountWithProvider(); const wrapped = rendered.find(Wrapped); - const intlProvider = rendered.find(IntlProvider).childAt(0); + const intl = rendered.state('intl'); - expect(wrapped.prop(propName)).toBe(intlProvider.state('context')); + expect(wrapped.prop(propName)).toBe(intl); }); }); @@ -138,9 +138,9 @@ describe('injectIntl()', () => { expect(wrapperRef.current).toBe(wrapped.instance()); - const intlProvider = rendered.find(IntlProvider).childAt(0); + const intl = rendered.state('intl'); - expect(wrapped.prop(propName)).toBe(intlProvider.state('context')); + expect(wrapped.prop(propName)).toBe(intl); }); }); }); diff --git a/test/unit/testUtils.tsx b/test/unit/testUtils.tsx index 73a6b019e2..dab9ca266c 100644 --- a/test/unit/testUtils.tsx +++ b/test/unit/testUtils.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import {mount} from 'enzyme'; -import {WithIntlProps} from '../../src/components/injectIntl'; import Provider, {Props as ProviderProps} from '../../src/components/provider'; function StrictProvider(props: ProviderProps) { @@ -14,10 +13,7 @@ function StrictProvider(props: ProviderProps) { export function mountFormattedComponentWithProvider

( Comp: React.ComponentType

) { - return ( - props: P, - providerProps: WithIntlProps = {locale: 'en'} - ) => { + return (props: P, providerProps: ProviderProps = {locale: 'en'}) => { return mount( , {