Skip to content

Commit 68ca746

Browse files
committed
translation context: Eliminate key hack (the last one!).
In putting together a recent commit, I realized that this would be possible if our `TranslationContextTranslator` more carefully spoke the language of the old Context API. By making it a `Component` instead of a `PureComponent`, and making sure there aren't any disruptive ancestors between it and IntlProvider (in particular, there are *no* ancestors in between) it'll be updated when `intl` changes. It already speaks the new Context API as well as it needs to (i.e., nothing special has to happen to get its consumers to update), and we don't have any consumers that directly consume the old one straight from `react-intl`. One necessary tweak was to stop using `this._`, which is only set in the constructor. (Reminiscent of b530f6c, when we were removing the `key` hack for styles.) Fixes: #1946
1 parent 855de67 commit 68ca746

File tree

2 files changed

+47
-42
lines changed

2 files changed

+47
-42
lines changed

docs/architecture/react.md

+14-21
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ long as the code adheres to core Redux principles:
103103
### Context
104104

105105
We're on board with the current API where possible; there's a
106-
third-party library we use that isn't there yet.
106+
third-party library we use that isn't there yet, but we deal with that
107+
at the edge by "translating" the old API to the new one.
107108

108109
#### Current Context API
109110

@@ -164,29 +165,21 @@ updates to the context, and not just by the consumer's own
164165
We have to think about the legacy Context API in just one place. The
165166
`react-intl` library's `IntlProvider` uses it to provide the `intl`
166167
context. The only consumer of `intl` is
167-
`TranslationContextTranslator`, but we've made that a `PureComponent`,
168-
so it doesn't get updated when `intl` changes. Our workaround until
169-
now has been a "`key` hack":
170-
171-
If `locale` changes, we make the entire React component tree at and
172-
below `IntlProvider` remount. (Not merely re-`render`: completely
173-
vanish and start over with `componentDidMount`; see the note at [this
174-
doc](https://5d4b5feba32acd0008d0df98--reactjs.netlify.app/docs/reconciliation.html)
175-
starting with "Keys should be stable, predictable, and unique".) We do
176-
this with the [`key`
177-
attribute](https://reactjs.org/docs/lists-and-keys.html), which isn't
178-
recommended for use except in lists.
179-
180-
In the next commit, we stop using the `key` hack and instead make
181-
`TranslationContextTranslator` "speak" the old API better.
168+
`TranslationContextTranslator`, which "speaks" the old API by being
169+
the direct child of `IntlProvider` and by being a `Component`, not a
170+
`PureComponent` (whose under-the-hood `shouldComponentUpdate` would
171+
suppress updates on context changes)—all to make sure that it
172+
re-`render`s whenever `intl` changes. Then,
173+
`TranslationContextTranslator` is itself a provider, and it provides
174+
the same value, but it does so in the new Context API way. All its
175+
consumers are updated appropriately, which is what we want.
182176

183177
### The exception: `MessageList`
184178

185-
We have one React component that we wrote (beyond `connect` calls) that
186-
deviates from the above design: `MessageList`. This is the only
187-
component that extends plain `Component` rather than `PureComponent`,
188-
and it's the only component in which we implement
189-
`shouldComponentUpdate`.
179+
We have one React component that we wrote (beyond `connect` calls)
180+
that deviates from the above design: `MessageList`. It extends plain
181+
`Component` rather than `PureComponent`, and it's the only component
182+
in which we implement `shouldComponentUpdate`.
190183

191184
In fact, `MessageList` does adhere to the Pure Component Principle -- its
192185
`render` method is a pure function of `this.props` and `this.context`. So

src/boot/TranslationProvider.js

+33-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* @flow strict-local */
2-
import React, { PureComponent } from 'react';
2+
import React, { PureComponent, Component } from 'react';
33
import type { ComponentType, ElementConfig, Node as React$Node } from 'react';
44
import { Text } from 'react-native';
55
import { IntlProvider } from 'react-intl';
@@ -51,9 +51,37 @@ const makeGetText = (intl: IntlShape): GetText => {
5151
* new API.
5252
*
5353
* See https://reactjs.org/docs/context.html
54-
* vs. https://reactjs.org/docs/legacy-context.html .
54+
* vs. https://reactjs.org/docs/legacy-context.html.
55+
*
56+
* Why do we need this? `IntlProvider` uses React's "legacy context
57+
* API", deprecated since React 16.3, of which the docs say:
58+
*
59+
* ## Updating Context
60+
*
61+
* Don't do it.
62+
*
63+
* React has an API to update context, but it is fundamentally
64+
* broken and you should not use it.
65+
*
66+
* It's broken because a consumer in the old way would never
67+
* re-`render` on changes to the context if they, or any of their
68+
* ancestors below the provider, implemented `shouldComponentUpdate`
69+
* in a way that blocked updates from the context. This meant that
70+
* neither the provider nor the consumer had the power to fix many
71+
* non-re-`render`ing bugs. A very common context-update-blocking
72+
* implementation of `shouldComponentUpdate` is the one
73+
* `PureComponent` uses, so the effect is widespread.
74+
*
75+
* In the new way, `shouldComponentUpdate`s (as implemented by hand or
76+
* by using `PureComponent`) in the hierarchy all the way down to the
77+
* consumer (inclusive) are ignored when the context updates.
78+
*
79+
* Consumers should consume `TranslationContext` as it's provided
80+
* here, so they don't have to worry about not updating when it
81+
* changes.
5582
*/
56-
class TranslationContextTranslator extends PureComponent<{|
83+
// This component MUST NOT be a `PureComponent`; see above.
84+
class TranslationContextTranslator extends Component<{|
5785
+children: React$Node,
5886
|}> {
5987
context: { intl: IntlShape };
@@ -62,11 +90,9 @@ class TranslationContextTranslator extends PureComponent<{|
6290
intl: () => null,
6391
};
6492

65-
_ = makeGetText(this.context.intl);
66-
6793
render() {
6894
return (
69-
<TranslationContext.Provider value={this._}>
95+
<TranslationContext.Provider value={makeGetText(this.context.intl)}>
7096
{this.props.children}
7197
</TranslationContext.Provider>
7298
);
@@ -84,21 +110,7 @@ class TranslationProvider extends PureComponent<Props> {
84110
const { locale, children } = this.props;
85111

86112
return (
87-
/* `IntlProvider` uses React's "legacy context API", deprecated since
88-
* React 16.3, of which the docs say:
89-
*
90-
* ## Updating Context
91-
*
92-
* Don't do it.
93-
*
94-
* React has an API to update context, but it is fundamentally
95-
* broken and you should not use it.
96-
*
97-
* To work around that, we set `key={locale}` to force the whole tree
98-
* to rerender if the locale changes. Not cheap, but the locale
99-
* changing is rare.
100-
*/
101-
<IntlProvider key={locale} locale={locale} textComponent={Text} messages={messages[locale]}>
113+
<IntlProvider locale={locale} textComponent={Text} messages={messages[locale]}>
102114
<TranslationContextTranslator>{children}</TranslationContextTranslator>
103115
</IntlProvider>
104116
);

0 commit comments

Comments
 (0)