diff --git a/.eslintrc b/.eslintrc index 945c2b0..93931a8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,12 +8,15 @@ "prettier" ], "plugins": [ - "prettier" + "prettier", + "react-hooks" ], "parser": "babel-eslint", "rules": { "import/prefer-default-export": "off", "prettier/prettier": "error", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", "react/destructuring-assignment": "off", "react/jsx-filename-extension": "off", "react/jsx-one-expression-per-line": "off", diff --git a/README.md b/README.md index 1d2e5fe..f5eb881 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,19 @@ An optional fetching lifecycle method. Invoked just after `GET /props.url` respo ##### `onFetchingError?: (e?: Error) => void` An optional fetching lifecycle method. Invoked when error occurs during fetching/processing stage. +## `useMessageSource(keyPrefix?: string): ComponentAPI` +A React Hook version of the [ComponentAPI](#ComponentApi). + +```js +import React from 'react' +import { useMessageSource } from 'react-message-source' + +function MyComponent() { + const { getMessage, getMessageWithNamedParams } = useMessageSource() + return ... +} +``` + ## `withMessages` Creates a higher order component and provides the [ComponentAPI](#ComponentAPI) as `props`. It can be used in two ways: @@ -102,7 +115,7 @@ Exposes the [ComponentAPI](#ComponentApi) as standard `prop-types` definition. #### `App.jsx` ```jsx import React, { Component } from 'react' -import * as MessageSource from 'react-message-source' +import { Provider as MessageSourceProvider } from 'react-message-source' import translations from './translations.json' @@ -112,11 +125,12 @@ import MyComponentWithNamedParams from './MyComponentWithNamedParams' export default function App() { return ( - + + - + ) } ``` @@ -124,7 +138,7 @@ export default function App() { #### `FetchApp.jsx` ```jsx import React, { Component } from 'react' -import * as MessageSource from 'react-message-source' +import { FetchingProvider as FetchingMessageSourceProvider } from 'react-message-source' import MyComponent from './MyComponent' import MyComponentWithIndexedParams from './MyComponentWithIndexedParams' @@ -132,11 +146,12 @@ import MyComponentWithNamedParams from './MyComponentWithNamedParams' export default function FetchApp() { return ( - + + - + ) } ``` @@ -154,6 +169,17 @@ function MyComponent(props) { export default withMessages(MyComponent) ``` +#### `MyComponentWithHooks.jsx` +```jsx +import React from 'react' +import { useMessageSource } from 'react-message-source' + +export default function MyComponent(props) { + const { getMessage } = useMessageSource(); + return {getMessage('hello.world')} +} +``` + #### `MyComponentWithIndexedParams.jsx` ```jsx import React from 'react' @@ -187,4 +213,4 @@ export default compose( ## License -MIT © [Netcetera AG](https://github.com/netceteragroup) +MIT © [Netcetera](https://github.com/netceteragroup) diff --git a/example/src/App.js b/example/src/App.js index 8a5dac7..63d3bfc 100644 --- a/example/src/App.js +++ b/example/src/App.js @@ -1,6 +1,7 @@ import React from 'react'; -import * as MessageSource from 'react-message-source'; +import { Provider as MessageSourceProvider } from 'react-message-source'; +import Hooks from './Hooks'; import { LocalizedLabel, LocalizedLabelCurried, @@ -18,15 +19,13 @@ export default function App() {

The content below is localized, see Greeting.js for more information.

- + - - - - + +
); } diff --git a/example/src/Hooks.js b/example/src/Hooks.js new file mode 100644 index 0000000..22957ef --- /dev/null +++ b/example/src/Hooks.js @@ -0,0 +1,7 @@ +import React from 'react'; +import { useMessageSource } from 'react-message-source'; + +export default function Hooks() { + const { getMessage } = useMessageSource(); + return Translation with a hook: {getMessage('hello.world')}; +} diff --git a/package.json b/package.json index 9037af8..a1b6f2c 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "peerDependencies": { "prop-types": "^15.5.10", - "react": "^16.6.0" + "react": "^16.8.0" }, "devDependencies": { "@babel/core": "^7.3.4", @@ -49,6 +49,7 @@ "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-prettier": "^3.0.1", "eslint-plugin-react": "^7.12.4", + "eslint-plugin-react-hooks": "^1.5.1", "prettier": "^1.16.4", "prop-types": "^15.7.2", "react": "^16.8.4", diff --git a/src/index.js b/src/index.js index a9db15a..8b078ef 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,8 @@ /** * The Public API. */ -export { Provider, withMessages, propTypes } from './lib/messageSource'; +export { Provider, Consumer } from './lib/MessageSourceContext'; export { FetchingProvider } from './lib/FetchingProvider'; +export { useMessageSource } from './lib/useMessageSource'; +export { withMessages } from './lib/withMessages'; +export { propTypes } from './lib/propTypes'; diff --git a/src/lib/FetchingProvider.js b/src/lib/FetchingProvider.js index b148b13..15afabe 100644 --- a/src/lib/FetchingProvider.js +++ b/src/lib/FetchingProvider.js @@ -1,112 +1,101 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; - -import { Provider } from './messageSource'; +import { Provider } from './MessageSourceContext'; const identity = x => x; -export class FetchingProvider extends Component { - state = { - translations: {}, - fetching: false, - }; - - mounted = false; - - static propTypes = { - /** - * The URL which serves the text messages. - * Required. - */ - url: PropTypes.string.isRequired, - - /** - * Makes the rendering of the sub-tree synchronous. - * The components will not render until fetching of the text messages finish. - * - * Defaults to true. - */ - blocking: PropTypes.bool, - - /** - * A function which can transform the response received from GET /props.url - * to a suitable format: - * - * Example: - * function transform(response) { - * return response.textMessages; - * } - */ - transform: PropTypes.func, - - /** - * Invoked when fetching of text messages starts. - */ - onFetchingStart: PropTypes.func, - - /** - * Invoked when fetching of text messages finishes. - */ - onFetchingEnd: PropTypes.func, - - /** - * Invoked when fetching fails. - */ - onFetchingError: PropTypes.func, - - /** - * Children. - */ - children: PropTypes.node, - }; - - static defaultProps = { - blocking: true, - transform: identity, - onFetchingStart: identity, - onFetchingEnd: identity, - onFetchingError: identity, - }; - - componentDidMount() { - this.mounted = true; - this.fetchResources(); - } - - componentDidUpdate(prevProps) { - if (this.props.url !== prevProps.url) { - this.fetchResources(); - } - } - - componentWillUnmount() { - this.mounted = false; - } - - fetchResources = () => { - const { url, transform, onFetchingStart, onFetchingEnd, onFetchingError } = this.props; - - this.setState({ fetching: true }, onFetchingStart); +const initialState = { + translations: {}, + isFetching: false, +}; + +/** + * A special which can load translations from remote URL + * via a `GET` request and pass them down the component tree. + */ +export function FetchingProvider(props) { + const { url, blocking, children, transform, onFetchingStart, onFetchingEnd, onFetchingError } = props; + const [{ translations, isFetching }, setState] = React.useState(initialState); + + React.useEffect(() => { + let isStillMounted = true; + + setState(state => ({ ...state, isFetching: true })); + onFetchingStart(); + fetch(url) .then(r => r.json()) .then(response => { - if (this.mounted) { - this.setState( - { - translations: transform(response), - fetching: false, - }, - onFetchingEnd, - ); + if (isStillMounted) { + setState({ + translations: transform(response), + isFetching: false, + }); + onFetchingEnd(); } }) .catch(onFetchingError); - }; - - render() { - const { blocking, children } = this.props; - const { translations, fetching } = this.state; - const shouldRenderSubtree = !blocking || (blocking && !fetching); - return {shouldRenderSubtree ? children : null}; - } + + return () => { + isStillMounted = false; + }; + }, [url]); // re-fetch only when url changes + + const shouldRenderSubtree = !blocking || (blocking && !isFetching); + return {shouldRenderSubtree ? children : null}; } + +FetchingProvider.propTypes = { + /** + * The URL which serves the text messages. + * Required. + */ + url: PropTypes.string.isRequired, + + /** + * Makes the rendering of the sub-tree synchronous. + * The components will not render until fetching of the text messages finish. + * + * Defaults to true. + */ + blocking: PropTypes.bool, + + /** + * A function which can transform the response received from GET /props.url + * to a suitable format: + * + * Example: + * function transform(response) { + * return response.textMessages; + * } + */ + transform: PropTypes.func, + + /** + * Invoked when fetching of text messages starts. + */ + onFetchingStart: PropTypes.func, + + /** + * Invoked when fetching of text messages finishes. + */ + onFetchingEnd: PropTypes.func, + + /** + * Invoked when fetching fails. + */ + onFetchingError: PropTypes.func, + + /** + * Children. + */ + children: PropTypes.node, +}; + +FetchingProvider.defaultProps = { + blocking: true, + transform: identity, + onFetchingStart: identity, + onFetchingEnd: identity, + onFetchingError: identity, +}; diff --git a/src/lib/FetchingProvider.test.js b/src/lib/FetchingProvider.test.js index 1237219..48b8216 100644 --- a/src/lib/FetchingProvider.test.js +++ b/src/lib/FetchingProvider.test.js @@ -1,10 +1,13 @@ import React from 'react'; import * as RTL from 'react-testing-library'; import { FetchingProvider } from './FetchingProvider'; -import { withMessages } from './messageSource'; +import { useMessageSource } from './useMessageSource'; describe('FetchingProvider', () => { - const Spy = withMessages(props => props.getMessage('hello.world')); + const Spy = () => { + const { getMessage } = useMessageSource(); + return getMessage('hello.world'); + }; beforeEach(() => { // mock impl of fetch() API @@ -48,10 +51,17 @@ describe('FetchingProvider', () => { }); it('fetches text resources when url prop changes', async () => { + const transform = jest.fn(x => x); + const onFetchingStart = jest.fn(); + const onFetchingEnd = jest.fn(); function TestComponent(props) { return ( - // eslint-disable-next-line react/prop-types - + ); @@ -60,9 +70,22 @@ describe('FetchingProvider', () => { const { getByText, rerender } = RTL.render(); await RTL.waitForElement(() => getByText('Hello world')); - rerender(); + RTL.act(() => { + rerender(); + }); + + await RTL.wait( + () => + new Promise(resolve => { + // simulate network request + setTimeout(() => resolve(), 300); + }), + ); expect(global.fetch).toHaveBeenCalledTimes(2); + expect(transform).toHaveBeenCalledTimes(2); + expect(onFetchingStart).toHaveBeenCalledTimes(2); + expect(onFetchingEnd).toHaveBeenCalledTimes(2); }); it('invokes onFetchingError lifecycle on network failure', async () => { diff --git a/src/lib/MessageSourceContext.js b/src/lib/MessageSourceContext.js new file mode 100644 index 0000000..fa3775b --- /dev/null +++ b/src/lib/MessageSourceContext.js @@ -0,0 +1,28 @@ +import React from 'react'; + +/** + * Initial Context value, an empty object. + */ +const empty = {}; + +/** + * A React Context which holds the translations map. + */ +const MessageSourceContext = React.createContext(empty); +MessageSourceContext.displayName = 'MessageSourceContext'; + +/** + * The MessageSourceContext object. + */ +export { MessageSourceContext }; + +/** + * Example usage: + * + * const translations = await fetch('/api/rest/texts?lang=en'); + * + * + * ... + * + */ +export const { Provider, Consumer } = MessageSourceContext; diff --git a/src/lib/messageSource.js b/src/lib/messageSource.js deleted file mode 100644 index 30dc81c..0000000 --- a/src/lib/messageSource.js +++ /dev/null @@ -1,146 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import invariant from 'invariant'; -import hoistNonReactStatics from 'hoist-non-react-statics'; -import { getMessageWithNamedParams, getMessageWithParams } from './messages'; -import { normalizeKeyPrefix } from './utils'; - -/** - * A React Context which holds the translations map. - */ -const MessageSourceContext = React.createContext(null); -MessageSourceContext.displayName = 'MessageSourceContext'; - -/** - * Creates a HOC which passes the MessageSourceApi to the given Component. - */ -function enhanceWithMessages(keyPrefix, WrappedComponent) { - const normalizedKeyPrefix = normalizeKeyPrefix(keyPrefix || ''); - const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; - - /** - * The enhancer HOC. - */ - class Enhancer extends React.Component { - /** - * Retrieves a text message. - * - * Example usage: - * let name, lastName; - * ... - * const message = getMessage('message.key', name, lastName); - * - * @param key the key of the message. - * @param params an optional parameters (param0, param1 ...). - */ - getMessage = (key, ...params) => { - const textKey = normalizedKeyPrefix + key; - const message = getMessageWithParams(this.context, textKey, ...params); - if (message === textKey) { - return getMessageWithParams(this.context, key, ...params); - } - - return message; - }; - - /** - * Retrieves a text message with named parameters. - * - * Example usage: - * const parameters = { - * name: 'John Doe', - * } - * - * const message = getMessageWithNamedParams('message.key', parameters) - * - * @param key the key of the message. - * @param namedParams a map of named parameters. - */ - getMessageWithNamedParams = (key, namedParams) => { - const textKey = normalizedKeyPrefix + key; - const message = getMessageWithNamedParams(this.context, textKey, namedParams); - if (message === textKey) { - return getMessageWithNamedParams(this.context, key, namedParams); - } - - return message; - }; - - render() { - if (process.env.NODE_ENV !== 'production') { - /* eslint-disable react/prop-types */ - invariant( - !this.props.getMessage, - `[react-message-source]: [%s] already has a prop named [getMessage]. It will be overwritten.`, - wrappedComponentName, - ); - - invariant( - !this.props.getMessageWithNamedParams, - `[react-message-source]: [%s] already has a prop named [getMessageWithNamedParams]. It will be overwritten.`, - wrappedComponentName, - ); - /* eslint-enable react/prop-types */ - } - - return ( - - ); - } - } - - Enhancer.contextType = MessageSourceContext; - Enhancer.displayName = `WithMessages(${wrappedComponentName})`; - - return hoistNonReactStatics(Enhancer, WrappedComponent); -} - -/** - * An internal implementation of the argument resolution logic. - * - * @param keyPrefixOrComponent the passed argument ([Component] | [[keyPrefix], [Component]]) - */ -function internalWithMessages(keyPrefixOrComponent) { - if (keyPrefixOrComponent == null || typeof keyPrefixOrComponent === 'string') { - // consumer used the curried API - return enhanceWithMessages.bind(undefined, keyPrefixOrComponent); - } - - return enhanceWithMessages(null, keyPrefixOrComponent); -} - -/** - * Example usage: - * - * const translations = await fetch('/api/rest/texts?lang=en'); - * - * - * ... - * - */ -export const { Provider } = MessageSourceContext; - -/** - * Example usages: - * - * 1. MessageSource.withMessages('keyPrefix')(Component) - * 2. MessageSource.withMessages(Component) - * 3. compose(MessageSource.withMessages('keyPrefix'))(Component) - * 4. compose(MessageSource.withMessages)(Component) - */ -export const withMessages = internalWithMessages; - -/** - * Example usage: - * - * Exported just for convenience, in case you want to run propType checks on your component. - * Note: some bundlers might remove these definitions during build time. - */ -export const propTypes = { - getMessage: PropTypes.func, - getMessageWithNamedParams: PropTypes.func, -}; diff --git a/src/lib/propTypes.js b/src/lib/propTypes.js new file mode 100644 index 0000000..fa43c18 --- /dev/null +++ b/src/lib/propTypes.js @@ -0,0 +1,12 @@ +import PropTypes from 'prop-types'; + +/** + * Example usage: + * + * Exported just for convenience, in case you want to run propType checks on your component. + * Note: some bundlers might remove these definitions during build time. + */ +export const propTypes = { + getMessage: PropTypes.func, + getMessageWithNamedParams: PropTypes.func, +}; diff --git a/src/lib/useMessageSource.js b/src/lib/useMessageSource.js new file mode 100644 index 0000000..d702bd7 --- /dev/null +++ b/src/lib/useMessageSource.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { MessageSourceContext } from './MessageSourceContext'; +import { getMessageWithNamedParams, getMessageWithParams } from './messages'; +import { normalizeKeyPrefix } from './utils'; + +/** + * A Hook which which provides the MessageSourceApi. + * + * @param keyPrefix an optional prefix which will be prepended to the lookup key. + */ +export function useMessageSource(keyPrefix) { + const textKeys = React.useContext(MessageSourceContext); + return React.useMemo(() => { + const keyPrefixToUse = normalizeKeyPrefix(keyPrefix || ''); + return { + /** + * Retrieves a text message. + * + * Example usage: + * let name, lastName; + * ... + * const message = getMessage('message.key', name, lastName); + * + * @param key the key of the message. + * @param params an optional parameters (param0, param1 ...). + */ + getMessage(key, ...params) { + const textKey = keyPrefixToUse + key; + const message = getMessageWithParams(textKeys, textKey, ...params); + if (message === textKey) { + // retry with key only (no prefix) + return getMessageWithParams(textKeys, key, ...params); + } + + return message; + }, + + /** + * Retrieves a text message with named parameters. + * + * Example usage: + * const parameters = { + * name: 'John Doe', + * } + * + * const message = getMessageWithNamedParams('message.key', parameters) + * + * @param key the key of the message. + * @param namedParams a map of named parameters. + */ + getMessageWithNamedParams(key, namedParams) { + const textKey = keyPrefixToUse + key; + const message = getMessageWithNamedParams(textKeys, textKey, namedParams); + if (message === textKey) { + // retry with key only (no prefix) + return getMessageWithNamedParams(textKeys, key, namedParams); + } + + return message; + }, + }; + }, [textKeys, keyPrefix]); +} diff --git a/src/lib/useMessageSource.test.js b/src/lib/useMessageSource.test.js new file mode 100644 index 0000000..053ee6d --- /dev/null +++ b/src/lib/useMessageSource.test.js @@ -0,0 +1,86 @@ +import React from 'react'; +import TestRenderer from 'react-test-renderer'; +import { useMessageSource } from './useMessageSource'; +import { Provider as MessageSourceProvider } from './MessageSourceContext'; +import { propTypes as MessageSourceApi } from './propTypes'; + +describe('useMessageSource', () => { + const translations = { + 'hello.world': 'Hello World', + 'greeting.normal': 'Hi', + 'greeting.named': 'Hello {name}', + }; + + it('exposes the correct api', () => { + function AssertApi() { + const hooksApi = useMessageSource(); + Object.keys(MessageSourceApi).forEach(api => { + expect(hooksApi[api]).toBeDefined(); + }); + + return null; + } + + TestRenderer.create( + + + , + ); + }); + + it('retrieves the correct translated value with named parameters', () => { + function Nested() { + const { getMessageWithNamedParams } = useMessageSource(); + return getMessageWithNamedParams('greeting.named', { + name: 'John Doe', + }); + } + + const renderer = TestRenderer.create( + + + , + ); + + const { root } = renderer; + const nestedComponentInstance = root.findByType(Nested); + + expect(nestedComponentInstance.children[0]).toBe('Hello John Doe'); + }); + + it('retrieves the correct translated value with prefix', () => { + function Nested() { + const { getMessage } = useMessageSource('hello'); + return getMessage('world'); + } + + const renderer = TestRenderer.create( + + + , + ); + + const { root } = renderer; + const nestedComponentInstance = root.findByType(Nested); + + expect(nestedComponentInstance.children[0]).toBe('Hello World'); + }); + + it('retrieves the correct translated value without prefix', () => { + function Nested() { + const { getMessage } = useMessageSource(); + return getMessage('hello.world'); + } + + const renderer = TestRenderer.create( + + + , + ); + + const { root } = renderer; + const nestedComponentInstance = root.findByType(Nested); + + expect(nestedComponentInstance.children[0]).toBe('Hello World'); + }); +}); diff --git a/src/lib/withMessages.js b/src/lib/withMessages.js new file mode 100644 index 0000000..cf47823 --- /dev/null +++ b/src/lib/withMessages.js @@ -0,0 +1,60 @@ +import React from 'react'; +import invariant from 'invariant'; +import hoistNonReactStatics from 'hoist-non-react-statics'; +import { useMessageSource } from './useMessageSource'; + +/** + * Creates a HOC which passes the MessageSourceApi to the given Component. + */ +function enhanceWithMessages(keyPrefix, WrappedComponent) { + const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; + + /** + * The enhancer HOC. + */ + function Enhancer(props) { + const messageSourceApi = useMessageSource(keyPrefix); + if (process.env.NODE_ENV !== 'production') { + const hasOwn = Object.prototype.hasOwnProperty; + const propsToOverwrite = Object.keys(messageSourceApi) + .filter(propToCheck => hasOwn.call(props, propToCheck)) + .join(', '); + + invariant( + !propsToOverwrite, + `[react-message-source]: [%s] already has props named [%s]. They will be overwritten.`, + wrappedComponentName, + propsToOverwrite, + ); + } + + return ; + } + + Enhancer.displayName = `WithMessages(${wrappedComponentName})`; + return hoistNonReactStatics(Enhancer, WrappedComponent); +} + +/** + * An internal implementation of the argument resolution logic. + * + * @param keyPrefixOrComponent the passed argument ([Component] | [[keyPrefix], [Component]]) + */ +function internalWithMessages(keyPrefixOrComponent) { + if (keyPrefixOrComponent == null || typeof keyPrefixOrComponent === 'string') { + // consumer used the curried API + return enhanceWithMessages.bind(undefined, keyPrefixOrComponent); + } + + return enhanceWithMessages(null, keyPrefixOrComponent); +} + +/** + * Example usages: + * + * 1. MessageSource.withMessages('keyPrefix')(Component) + * 2. MessageSource.withMessages(Component) + * 3. compose(MessageSource.withMessages('keyPrefix'))(Component) + * 4. compose(MessageSource.withMessages)(Component) + */ +export const withMessages = internalWithMessages; diff --git a/src/lib/messageSource.test.js b/src/lib/withMessages.test.js similarity index 79% rename from src/lib/messageSource.test.js rename to src/lib/withMessages.test.js index 8965026..f28675a 100644 --- a/src/lib/messageSource.test.js +++ b/src/lib/withMessages.test.js @@ -1,10 +1,10 @@ import React from 'react'; import TestRenderer from 'react-test-renderer'; -import * as MessageSource from './messageSource'; +import { Provider as MessageSourceProvider } from './MessageSourceContext'; +import * as MessageSource from './withMessages'; +import { propTypes as MessageSourceApi } from './propTypes'; -/* eslint-disable react/prop-types */ - -describe('MessageSource', () => { +describe('withMessages', () => { const translations = { 'hello.world': 'Hello World', 'greeting.normal': 'Hi', @@ -19,8 +19,9 @@ describe('MessageSource', () => { const { root } = renderer; const captorInstance = root.findByType(PropsCaptor); - expect(captorInstance.props.getMessage).toBeDefined(); - expect(captorInstance.props.getMessageWithNamedParams).toBeDefined(); + Object.keys(MessageSourceApi).forEach(api => { + expect(captorInstance.props[api]).toBeDefined(); + }); }); it('retrieves the correct translated value with named parameters', () => { @@ -34,9 +35,9 @@ describe('MessageSource', () => { const NestedHOC = MessageSource.withMessages(Nested); const renderer = TestRenderer.create( - + - , + , ); const { root } = renderer; @@ -54,9 +55,9 @@ describe('MessageSource', () => { const NestedHOC = MessageSource.withMessages(Nested); const renderer = TestRenderer.create( - + - , + , ); const { root } = renderer; @@ -74,9 +75,9 @@ describe('MessageSource', () => { const NestedHOC = MessageSource.withMessages()(Nested); const renderer = TestRenderer.create( - + - , + , ); const { root } = renderer; @@ -94,9 +95,9 @@ describe('MessageSource', () => { const NestedHOC = MessageSource.withMessages('hello')(Nested); const renderer = TestRenderer.create( - + - , + , ); const { root } = renderer; @@ -107,7 +108,7 @@ describe('MessageSource', () => { it('[curried] retrieves the correct translated value in mixed mode', () => { function Nested(props) { - const { getMessage } = props; + const { getMessage } = props; // eslint-disable-line react/prop-types return ( {getMessage('world')} @@ -119,9 +120,9 @@ describe('MessageSource', () => { const NestedHOC = MessageSource.withMessages('hello')(Nested); const renderer = TestRenderer.create( - + - , + , ); const { root } = renderer; @@ -148,12 +149,12 @@ describe('MessageSource', () => { const NestedHOC = MessageSource.withMessages()(Nested); const renderer = TestRenderer.create( - + - + - - , + + , ); const components = renderer.root.findAllByType(Nested); @@ -163,9 +164,4 @@ describe('MessageSource', () => { expect(levelOneComponent.children[0]).toBe('Hello World'); expect(levelTwoComponent.children[0]).toBe('Hallo Welt'); }); - - it('propTypes are exported', () => { - // eslint-disable-next-line react/forbid-foreign-prop-types - expect(MessageSource.propTypes).toBeDefined(); - }); }); diff --git a/yarn.lock b/yarn.lock index dc6218a..3d49b73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3527,6 +3527,11 @@ eslint-plugin-prettier@^3.0.1: dependencies: prettier-linter-helpers "^1.0.0" +eslint-plugin-react-hooks@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.5.1.tgz#3c601326914ee0e1fedd709115db4940bdbbed4a" + integrity sha512-i3dIrmZ+Ssrm0LrbbtuGcRf7EEpe1FaMuL8XnnpZO0X4tk3dZNzevWxD0/7nMAFa5yZQfNnYkfEP0MmwLvbdHw== + eslint-plugin-react@7.12.4, eslint-plugin-react@^7.12.4: version "7.12.4" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.12.4.tgz#b1ecf26479d61aee650da612e425c53a99f48c8c"