Skip to content

Commit ecb5bde

Browse files
committed
Memoize the whole Provider component instead of each prop
It simplifies the code a lot and might also make it faster, as memoization during render comes at the cost of additional array and function scope allocation. This also adds a test to check that the Provider is not re-rendered when the bundles prop does not change.
1 parent b23bf28 commit ecb5bde

File tree

2 files changed

+44
-42
lines changed

2 files changed

+44
-42
lines changed

fluent-react/src/provider.js

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CachedSyncIterable } from "cached-iterable";
2-
import { createElement, useMemo } from "react";
2+
import { createElement, memo } from "react";
33
import PropTypes from "prop-types";
44
import { mapBundleSync } from "@fluent/sequence";
55
import FluentContext from "./context";
@@ -23,7 +23,7 @@ import createParseMarkup from "./markup";
2323
* `ReactLocalization` to format translations. If a translation is missing in
2424
* one instance, `ReactLocalization` will fall back to the next one.
2525
*/
26-
export default function LocalizationProvider(props) {
26+
function LocalizationProvider(props) {
2727
if (props.bundles === undefined) {
2828
throw new Error("LocalizationProvider must receive the bundles prop.");
2929
}
@@ -32,52 +32,42 @@ export default function LocalizationProvider(props) {
3232
throw new Error("The bundles prop must be an iterable.");
3333
}
3434

35-
const bundles = useMemo(() => CachedSyncIterable.from(props.bundles), [
36-
props.bundles
37-
]);
38-
const parseMarkup = useMemo(() => props.parseMarkup || createParseMarkup(), [
39-
props.parseMarkup
40-
]);
41-
const contextValue = useMemo(
42-
() => {
43-
const l10n = {
44-
getBundle: id => mapBundleSync(bundles, id),
45-
getString(id, args, fallback) {
46-
const bundle = l10n.getBundle(id);
35+
const bundles = CachedSyncIterable.from(props.bundles);
36+
const parseMarkup = props.parseMarkup || createParseMarkup();
37+
const l10n = {
38+
getBundle: id => mapBundleSync(bundles, id),
39+
getString(id, args, fallback) {
40+
const bundle = l10n.getBundle(id);
4741

48-
if (bundle) {
49-
const msg = bundle.getMessage(id);
50-
if (msg && msg.value) {
51-
let errors = [];
52-
let value = bundle.formatPattern(msg.value, args, errors);
53-
for (let error of errors) {
54-
l10n.reportError(error);
55-
}
56-
return value;
57-
}
42+
if (bundle) {
43+
const msg = bundle.getMessage(id);
44+
if (msg && msg.value) {
45+
let errors = [];
46+
let value = bundle.formatPattern(msg.value, args, errors);
47+
for (let error of errors) {
48+
l10n.reportError(error);
5849
}
59-
60-
return fallback || id;
61-
},
62-
// XXX Control this via a prop passed to the LocalizationProvider.
63-
// See https://github.com/projectfluent/fluent.js/issues/411.
64-
reportError(error) {
65-
/* global console */
66-
// eslint-disable-next-line no-console
67-
console.warn(`[@fluent/react] ${error.name}: ${error.message}`);
50+
return value;
6851
}
69-
};
70-
return {
71-
l10n,
72-
parseMarkup
73-
};
52+
}
53+
54+
return fallback || id;
7455
},
75-
[bundles, parseMarkup]
76-
);
56+
// XXX Control this via a prop passed to the LocalizationProvider.
57+
// See https://github.com/projectfluent/fluent.js/issues/411.
58+
reportError(error) {
59+
/* global console */
60+
// eslint-disable-next-line no-console
61+
console.warn(`[@fluent/react] ${error.name}: ${error.message}`);
62+
}
63+
};
7764

7865
return createElement(
7966
FluentContext.Provider,
80-
{ value: contextValue },
67+
{ value: {
68+
l10n,
69+
parseMarkup
70+
} },
8171
props.children
8272
);
8373
}
@@ -105,3 +95,5 @@ function isIterable(props, propName, componentName) {
10595
`The ${propName} prop supplied to ${componentName} must be an iterable.`
10696
);
10797
}
98+
99+
export default memo(LocalizationProvider);

fluent-react/test/provider_valid.test.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react";
2-
import TestRenderer from "react-test-renderer";
2+
import TestRenderer, {act} from "react-test-renderer";
33
import { LocalizationProvider } from "../src/index";
44

55
describe("LocalizationProvider - validation", () => {
@@ -43,4 +43,14 @@ describe("LocalizationProvider - validation", () => {
4343
TestRenderer.create(<LocalizationProvider bundles={0} />);
4444
}).toThrow(/must be an iterable/);
4545
});
46+
47+
test("is memoized (no re-render) when props are the same", () => {
48+
const bundles = [];
49+
const spy = jest.spyOn(bundles, Symbol.iterator);
50+
let renderer = TestRenderer.create(<LocalizationProvider bundles={bundles} />);
51+
act(() => {
52+
renderer = renderer.update(<LocalizationProvider bundles={bundles} />);
53+
});
54+
expect(spy).toHaveBeenCalledTimes(1);
55+
});
4656
});

0 commit comments

Comments
 (0)