formatjs/formatjs

Changing locale and FormattedMessage not updated instantly

Closed this issue · 14 comments

Not sure if it is an expected behavior. Here is the code setup:

locales['en'] = {"basic.text": "En"}
locales['zh'] = {"basic.text": "中文"}
// App.js
<IntlProvider locale={this.state.locale} messages={locales[this.state.locale]}
    // Some other wrappers 
     <FormattedMessage id="basic.text"/> 
</IntlProvider>

When I change the locale through (after the component is mounted):

this.setState({
    locale: "zh" // Or "en"
});

The text is not updated instantly. When I trigger some other related UI update, which result in the re-render of that FormattedMessage, the update can be seen.

I have tried to call forceUpdate() after changing state, but it doesn't work.

ericf commented

Can you create a reproducible example or failing test case?

In a word: I am closing this issue because there is not enough evidence supporting that this is a react-intl problem.
--------------------------longer version (optional)---------------------
I tried to create a simple example to reproduce the bug but I failed. Today I spent more time looking into this problem. In my project, only some FormattedMessage* didn't get updated instantly.
The difference between these "unfriendly" FormattedMessage and those normal FormattedMessage is that these bad FormattedMessage are put in a component that is wrapped by connect from react-redux.

const ReduxHeader = connect(mapStateToProps)(Header);

I couldn't figure out the reason and I couldn't reproduce the bug. (Sadly)
But I fixed this issue by adding locale prop to the component:

Header.propTypes = {
  ...,
  ...,
  locale: React.PropTypes.string // PURE BLACK MAGIC
}

And in the top level component I passed the locale to this component.

<Header locale={this.state.locale}/>

So it will force the header to re-render when locale is changed.

ericf commented

The only thing I am guessing it might be is the problem with shouldComponentUpdate and context:
facebook/react#2517

djhi commented

I've used another workaround for this. I call injectIntl for all my redux connected components like this:

export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(MyComponent));

Now all my FormattedMessage components update correctly when switching locale

ericf commented

@djhi Good idea! This works because the wrapper component created in injectIntl does not implement shouldComponentUpdate.

This is probably worth adding somewhere in the docs on the Wiki. If you find a good place to do so, feel free to add it.

djhi commented

Thx, I will do that, maybe tomorrow

I've used another workaround for this. I call injectIntl for all my redux connected components like this:

export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(MyComponent));

I found myself choosing between the workaround by @djhi and passing { pure: false } to connect() to bypass its shouldComponentUpdate, as suggested in react-redux Troubleshooting.

As far as I can tell, wrapping with injectIntl() specifically passes intl-context as a prop to the wrapped component (https://github.com/yahoo/react-intl/blob/master/src/inject.js#L45), which means the wrapped component is still pure.

Hence, when the only context that needs to be passed is intl, the above should be a cleaner fix than disabling shouldComponentUpdate in connect(), do you agree with that?

I spent a long time debugging this exact issue, and @djhi's solution saved me. Has this been added to the docs? If so, it's hard to find. (That or I'm just blind.)

ericf commented

@nwshane please add it to the docs, the Wiki can be updated by anyone.

I didn't quite like the idea of wrapping every single component in injectIntl, but the advice on using key={ locale } for IntlProvider from here worked like a charm.

TomiS commented

Wondering if it would help if FormattedMessage would be changed to use injectIntl internally instead of context?

EDIT: Probably not because injectIntl also uses context internally

@dubov94 you don't need to wrap every component in injectIntl just the those who implement some kind of shouldComponentUpdate logic that blocks the subtree from rendering, like connect from redux. And I think it is a way better approach since this just triggers the rerendering (changing existing dom elements) while the key change tears the whole dom down to build it new.

@TomiS no, it wouldn't. InjectIntl is using the context as well. You just need to get the context changes passt those Components which implement a shouldComponentUpdate because if those don't rerender all components below wont either. Thats why they don't see the context change.

I didn't quite like the idea of wrapping every single component in injectIntl, but the advice on using key={ locale } for IntlProvider from here worked like a charm.

I understand that it could help in some specific scenarios, but take care. It is dangerous because it will discard all inner component state when the language changes.