From a682e10b9238f179cca1aad675702748275db2ed Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Fri, 15 Mar 2019 23:07:26 +0100 Subject: [PATCH 1/9] introduce React Hooks and expose useMessageSource hook --- package.json | 2 +- src/index.js | 4 +- src/lib/FetchingProvider.js | 2 +- src/lib/MessageSourceContext.js | 28 ++++++++ src/lib/messageSource.js | 106 +++++-------------------------- src/lib/messageSource.test.js | 31 ++++----- src/lib/useMessageSource.js | 63 ++++++++++++++++++ src/lib/useMessageSource.test.js | 68 ++++++++++++++++++++ 8 files changed, 196 insertions(+), 108 deletions(-) create mode 100644 src/lib/MessageSourceContext.js create mode 100644 src/lib/useMessageSource.js create mode 100644 src/lib/useMessageSource.test.js diff --git a/package.json b/package.json index 9037af8..b092cb5 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", diff --git a/src/index.js b/src/index.js index a9db15a..db3d589 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,7 @@ /** * The Public API. */ -export { Provider, withMessages, propTypes } from './lib/messageSource'; +export { Provider } from './lib/MessageSourceContext'; export { FetchingProvider } from './lib/FetchingProvider'; +export { useMessageSource } from './lib/useMessageSource'; +export { withMessages, propTypes } from './lib/messageSource'; diff --git a/src/lib/FetchingProvider.js b/src/lib/FetchingProvider.js index b148b13..8a182bd 100644 --- a/src/lib/FetchingProvider.js +++ b/src/lib/FetchingProvider.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { Provider } from './messageSource'; +import { Provider } from './MessageSourceContext'; const identity = x => x; diff --git a/src/lib/MessageSourceContext.js b/src/lib/MessageSourceContext.js new file mode 100644 index 0000000..2b8b939 --- /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 } = MessageSourceContext; diff --git a/src/lib/messageSource.js b/src/lib/messageSource.js index 30dc81c..9fb124a 100644 --- a/src/lib/messageSource.js +++ b/src/lib/messageSource.js @@ -2,100 +2,37 @@ 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'; +import { useMessageSource } from './useMessageSource'; /** * 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 ( - + 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.contextType = MessageSourceContext; Enhancer.displayName = `WithMessages(${wrappedComponentName})`; - return hoistNonReactStatics(Enhancer, WrappedComponent); } @@ -113,17 +50,6 @@ function internalWithMessages(keyPrefixOrComponent) { return enhanceWithMessages(null, keyPrefixOrComponent); } -/** - * Example usage: - * - * const translations = await fetch('/api/rest/texts?lang=en'); - * - * - * ... - * - */ -export const { Provider } = MessageSourceContext; - /** * Example usages: * diff --git a/src/lib/messageSource.test.js b/src/lib/messageSource.test.js index 8965026..44723c5 100644 --- a/src/lib/messageSource.test.js +++ b/src/lib/messageSource.test.js @@ -1,10 +1,11 @@ import React from 'react'; import TestRenderer from 'react-test-renderer'; +import { Provider as MessageSourceProvider } from './MessageSourceContext'; import * as MessageSource from './messageSource'; /* eslint-disable react/prop-types */ -describe('MessageSource', () => { +describe('withMessages', () => { const translations = { 'hello.world': 'Hello World', 'greeting.normal': 'Hi', @@ -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; @@ -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); 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..bf41082 --- /dev/null +++ b/src/lib/useMessageSource.test.js @@ -0,0 +1,68 @@ +import React from 'react'; +import TestRenderer from 'react-test-renderer'; +import { useMessageSource } from './useMessageSource'; +import { Provider as MessageSourceProvider } from './MessageSourceContext'; + +describe('useMessageSource', () => { + const translations = { + 'hello.world': 'Hello World', + 'greeting.normal': 'Hi', + 'greeting.named': 'Hello {name}', + }; + + 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'); + }); +}); From 6fc2dce37e6359269133a6ff259ebf3ccba1ab08 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Fri, 15 Mar 2019 23:34:36 +0100 Subject: [PATCH 2/9] messageSource -> withMessages --- src/index.js | 2 +- src/lib/{messageSource.js => withMessages.js} | 0 src/lib/{messageSource.test.js => withMessages.test.js} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/lib/{messageSource.js => withMessages.js} (100%) rename src/lib/{messageSource.test.js => withMessages.test.js} (100%) diff --git a/src/index.js b/src/index.js index db3d589..35f11b6 100644 --- a/src/index.js +++ b/src/index.js @@ -4,4 +4,4 @@ export { Provider } from './lib/MessageSourceContext'; export { FetchingProvider } from './lib/FetchingProvider'; export { useMessageSource } from './lib/useMessageSource'; -export { withMessages, propTypes } from './lib/messageSource'; +export { withMessages, propTypes } from './lib/withMessages'; diff --git a/src/lib/messageSource.js b/src/lib/withMessages.js similarity index 100% rename from src/lib/messageSource.js rename to src/lib/withMessages.js diff --git a/src/lib/messageSource.test.js b/src/lib/withMessages.test.js similarity index 100% rename from src/lib/messageSource.test.js rename to src/lib/withMessages.test.js From bc132dcadc53cb854faed7224e5ddc2d6eddfe54 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Sat, 16 Mar 2019 00:17:14 +0100 Subject: [PATCH 3/9] include react-hooks eslint plugin --- .eslintrc | 5 ++++- package.json | 1 + yarn.lock | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) 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/package.json b/package.json index b092cb5..a1b6f2c 100644 --- a/package.json +++ b/package.json @@ -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/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" From 595748b580fae68f9e6ecfe1e540ae1b0711713e Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Sat, 16 Mar 2019 02:02:14 +0100 Subject: [PATCH 4/9] rewrite FetchingProvider with Hooks --- src/lib/FetchingProvider.js | 189 +++++++++++++++---------------- src/lib/FetchingProvider.test.js | 23 +++- 2 files changed, 108 insertions(+), 104 deletions(-) diff --git a/src/lib/FetchingProvider.js b/src/lib/FetchingProvider.js index 8a182bd..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 './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..133e847 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 - + ); @@ -62,7 +72,12 @@ describe('FetchingProvider', () => { rerender(); + await RTL.waitForElement(() => getByText('Hello world')); + 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 () => { From 168f7432e58025125be8cfebb26782ddd8ccabf9 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Sat, 16 Mar 2019 02:13:14 +0100 Subject: [PATCH 5/9] restructure propTypes api --- src/index.js | 3 ++- src/lib/propTypes.js | 12 ++++++++++++ src/lib/useMessageSource.test.js | 18 ++++++++++++++++++ src/lib/withMessages.js | 12 ------------ src/lib/withMessages.test.js | 17 ++++++----------- 5 files changed, 38 insertions(+), 24 deletions(-) create mode 100644 src/lib/propTypes.js diff --git a/src/index.js b/src/index.js index 35f11b6..439fa2c 100644 --- a/src/index.js +++ b/src/index.js @@ -4,4 +4,5 @@ export { Provider } from './lib/MessageSourceContext'; export { FetchingProvider } from './lib/FetchingProvider'; export { useMessageSource } from './lib/useMessageSource'; -export { withMessages, propTypes } from './lib/withMessages'; +export { withMessages } from './lib/withMessages'; +export { propTypes } from './lib/propTypes'; 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.test.js b/src/lib/useMessageSource.test.js index bf41082..053ee6d 100644 --- a/src/lib/useMessageSource.test.js +++ b/src/lib/useMessageSource.test.js @@ -2,6 +2,7 @@ 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 = { @@ -10,6 +11,23 @@ describe('useMessageSource', () => { '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(); diff --git a/src/lib/withMessages.js b/src/lib/withMessages.js index 9fb124a..cf47823 100644 --- a/src/lib/withMessages.js +++ b/src/lib/withMessages.js @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import invariant from 'invariant'; import hoistNonReactStatics from 'hoist-non-react-statics'; import { useMessageSource } from './useMessageSource'; @@ -59,14 +58,3 @@ function internalWithMessages(keyPrefixOrComponent) { * 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/withMessages.test.js b/src/lib/withMessages.test.js index 44723c5..f28675a 100644 --- a/src/lib/withMessages.test.js +++ b/src/lib/withMessages.test.js @@ -1,9 +1,8 @@ import React from 'react'; import TestRenderer from 'react-test-renderer'; import { Provider as MessageSourceProvider } from './MessageSourceContext'; -import * as MessageSource from './messageSource'; - -/* eslint-disable react/prop-types */ +import * as MessageSource from './withMessages'; +import { propTypes as MessageSourceApi } from './propTypes'; describe('withMessages', () => { const translations = { @@ -20,8 +19,9 @@ describe('withMessages', () => { 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', () => { @@ -108,7 +108,7 @@ describe('withMessages', () => { 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')} @@ -164,9 +164,4 @@ describe('withMessages', () => { 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(); - }); }); From 754844ffd3c15b4c493c7c9e4cd15c096e56cc40 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Sat, 16 Mar 2019 13:32:24 +0100 Subject: [PATCH 6/9] expose MessageSourceContext Consumer as part of the public API --- src/index.js | 2 +- src/lib/MessageSourceContext.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 439fa2c..8b078ef 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ /** * The Public API. */ -export { Provider } from './lib/MessageSourceContext'; +export { Provider, Consumer } from './lib/MessageSourceContext'; export { FetchingProvider } from './lib/FetchingProvider'; export { useMessageSource } from './lib/useMessageSource'; export { withMessages } from './lib/withMessages'; diff --git a/src/lib/MessageSourceContext.js b/src/lib/MessageSourceContext.js index 2b8b939..fa3775b 100644 --- a/src/lib/MessageSourceContext.js +++ b/src/lib/MessageSourceContext.js @@ -25,4 +25,4 @@ export { MessageSourceContext }; * ... * */ -export const { Provider } = MessageSourceContext; +export const { Provider, Consumer } = MessageSourceContext; From 5898a0d3069de05c95052080120d8d1bb6285f64 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Sat, 16 Mar 2019 13:48:38 +0100 Subject: [PATCH 7/9] simulate network request in test --- src/lib/FetchingProvider.test.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/lib/FetchingProvider.test.js b/src/lib/FetchingProvider.test.js index 133e847..48b8216 100644 --- a/src/lib/FetchingProvider.test.js +++ b/src/lib/FetchingProvider.test.js @@ -70,9 +70,17 @@ describe('FetchingProvider', () => { const { getByText, rerender } = RTL.render(); await RTL.waitForElement(() => getByText('Hello world')); - rerender(); + RTL.act(() => { + rerender(); + }); - await RTL.waitForElement(() => getByText('Hello world')); + await RTL.wait( + () => + new Promise(resolve => { + // simulate network request + setTimeout(() => resolve(), 300); + }), + ); expect(global.fetch).toHaveBeenCalledTimes(2); expect(transform).toHaveBeenCalledTimes(2); From b54d50f651315184a80ff1ab350b73db8311667c Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Sat, 16 Mar 2019 13:49:04 +0100 Subject: [PATCH 8/9] documents the useMessageSource hook API --- README.md | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) 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) From 0ffce95e8dc98c55442f431a8132b0054bd71f77 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Sat, 16 Mar 2019 13:52:24 +0100 Subject: [PATCH 9/9] add more examples for hooks --- example/src/App.js | 11 +++++------ example/src/Hooks.js | 7 +++++++ 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 example/src/Hooks.js 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')}; +}