Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is the recommended way to integrate FormatJS? #484

Closed
alanhogan opened this issue Nov 19, 2014 · 39 comments
Closed

What is the recommended way to integrate FormatJS? #484

alanhogan opened this issue Nov 19, 2014 · 39 comments

Comments

@alanhogan
Copy link

FormatJS illustrates the following example integration with React, using their React mixin (react-intl).

      var i18n = {
        locales: ["en-US"],
        messages: {
          SHORT: "{product} cost {price, number, usd} if ordered by {deadline, date, medium}",
          LONG: "{product} cost {price, number, usd} (or {price, number, eur}) if ordered by {deadline, date, medium}"
        }
      };

      React.renderComponent(
        Container({
          locales: i18n.locales,
          messages: i18n.messages
        }),
        document.getElementById('example')
      );

where Container uses the ReactIntlMixin mixin.

Any children of Container get the i18n data (locales & messages) for free (if they also use the ReactIntlMixin) thanks to react-intl's usage of React’s context.

I am experiencing difficulty integrating FormatJS into my React-Router-based project. The ridiculously naïve approach of attaching locales and messages as props to my Router fails to provide the necessary context.

Is my only option setting this data as props on my actual handlers?

@justinbelcher
Copy link

FWIW I wrap my Routes in a top-level Route for this reason (and to handle a little bit of global render-dependent bootstrapping).

@alanhogan
Copy link
Author

Thanks, @Jbelcher. I was converging there, as well, so that gives me some confidence I’m not making a mistake here :)

@gaearon
Copy link
Contributor

gaearon commented Nov 19, 2014

I also do that.

@ryanflorence
Copy link
Member

the new API in 0.11 makes this simple:

Router.run(routes, function(Handler) {
  React.render(<Handler locales={i18n.locales}/>, document.body);
});

@gpbl
Copy link

gpbl commented Dec 20, 2014

@gaearon @rpflorence any idea how to update that <Handler> props to re-render the whole app – for example, when the user changes the language? How could I set the props for <Handler/> ?

If I call setProps within the root route's handler, it warns it's an anti pattern since it's a child of other components. (or what could be an alternative for switching the language without reloading the page?)

@alanhogan
Copy link
Author

I asked about this more generally on the Reacr-Intl project and basically they intend you to reload the page to switch UI language

Alan

Le 20 déc. 2014 à 13:08, Giampaolo Bellavite notifications@github.com a écrit :

@gaearon @rpflorence any idea how to update that props to re-render the whole app – for example, when the user changes the language? How could I set the props for ?

If I call setProps within the root route's handler, it warns it's an anti pattern since it's a child of other components. (or what could be an alternative for switching the language without reloading the page?)


Reply to this email directly or view it on GitHub.

@gaearon
Copy link
Contributor

gaearon commented Dec 20, 2014

@gpbl

any idea how to update that <Handler> props to re-render the whole app – for example, when the user changes the language? How could I set the props for <Handler/> ?

Why not do this?

var CurrentHandler = null;
function renderApp() {
  if (CurrentHandler) {
    React.render(<CurrentHandler someProp={something} />, document.body);
  }
}

Router.run(routes, function(Handler) {
  CurrentHandler = Handler;
  renderApp();
});

something.onChange(function () {
  renderApp();
});

@gpbl
Copy link

gpbl commented Dec 20, 2014

wow @gaearon awesome !! that was simple! 👍

@gaearon
Copy link
Contributor

gaearon commented Dec 20, 2014

Yeah I saw @rpflorence write a very similar example in some other issue. Probably worth adding a section to the docs, since it's a very common pattern!

@DEllement
Copy link

as for Version 1.0.0 beta3, we can do:

var DefaultMessages = require("./../i18n/en");

var App = React.createClass({
    mixins: [IntlMixin, State],
    contextTypes: {
        router: React.PropTypes.func.isRequired
    },
    getDefaultProps: function(){
        return {
            locales:["en-CA"],
            messages:DefaultMessages
        };
    },
    render: function(){ ... }
});

@seethroughdev
Copy link

Hi @DEllement , can you explain more of what you did to get IntlMixin to work with 1.0.0 beta3?

I can't seem to pass the {locales, formats} object for react-intl with the mixin at all in the router, works fine without using the router though.

@DEllement
Copy link

Hi, basically, App is my main Application Component it hold my global state for the whole application. getDefaultProps is called the first time the Component is created which return an obj with 2 properties : locales & messages. As long as those 2 properties return the correct objects ( an array of string & a json object ).

Others in that thread was suggesting to pass those object as properties in the Handler rendering call.

It didn't seam possible in 1.0.0beta3 so i decided that it will do the same things to pass those props inside getDefaultProps of my main Application Component. Although it make your component tightly coupled with those properties i don't think its a real problem for now.

Also take note that your ReactComponent need the IntlMixin

Sorry for reply 6 days later and i hope it help somebody. :)

@garbin
Copy link

garbin commented Aug 14, 2015

@DEllement Your way will cause a warning, Like below

owner-based and parent-based contexts differ (values: `undefined` vs `en`) for key (locales) while mounting FormattedNumber

It because IntlMixin requires a context from owner, but in this situation, your component's owner is React Router, But not your App component.

My way is add intl context part to react Router object, because it's your component's owner, In my project, it works perfectly, no warning, no tightly coupled, and your nested route will get the rignt intl context data, it will works as you expected too.

Here is my code. I think this is the best way to integrated react-intl and react-router, any idea?

import {Router as ReactRouter, Route} from 'react-router';

class Router extends ReactRouter {
  static get childContextTypes(){
    return Object.assign(super.childContextTypes, {
      locales: React.PropTypes.oneOfType([
        React.PropTypes.string,
        React.PropTypes.array
      ]),
      formats : React.PropTypes.object,
      messages: React.PropTypes.object
    });
  }
  getChildContext(){
    var context = super.getChildContext() || {};
    var intl    = this.props.intl || {};
    context.locales = intl.locales || ['zh-CN'];
    context.formats = intl.formats || {};
    context.messages= intl.messages|| {};

    return context;
  }
}

React.render((
  <Router intl={{locales:...}} ...OTHER PROPS HERE>
     ... YOUR ROUTES HERE
  </Router>
),document.body);

@garbin
Copy link

garbin commented Aug 21, 2015

Sorry, I'm using 1.0.0 beta3, I have not tested it on 0.13.3. You could
upgrade 1.0.0 and give it a try:)

On Fri, Aug 21, 2015 at 10:35 PM, apreg notifications@github.com wrote:

@garbin https://github.com/garbin I tried your solution but it gives me Uncaught
TypeError: Super expression must either be null or a function, not undefined.
I'm using react-router ^0.13.3.


Reply to this email directly or view it on GitHub
#484 (comment).

@apreg
Copy link

apreg commented Aug 21, 2015

Yeah, I just did that. Sorry for my lame question. By the way after the upgrade I get Uncaught Error: Invariant Violation: Server-side <Router>s need location, branch, params, and components props. Try using Router.run to get all the props you need

var intlData = {
  locales: ['de', 'en'],
  messages: strings
};
React.render((
  <Router intl={intlData} >
...``` Now I'm trying to figure out how to prepare those props.

@garbin
Copy link

garbin commented Aug 21, 2015

Sorry, I only use react-router in client side..., so afraid I can't answer
this question. perhaps other peoples can give you answer :)

On Sat, Aug 22, 2015 at 2:07 AM, apreg notifications@github.com wrote:

Yeah, I just did that. Sorry for my lame question. By the way after the
upgrade I get Uncaught Error: Invariant Violation: Server-side s
need location, branch, params, and components props. Try using Router.run
to get all the props you need

var intlData = {
locales: ['de', 'en'],
messages: strings
};
React.render((

...```


Reply to this email directly or view it on GitHub
#484 (comment).

@apreg
Copy link

apreg commented Aug 21, 2015

I use it on client side too. :)

@apreg
Copy link

apreg commented Aug 22, 2015

Adding history prop was the key. So, it does not throw those exceptions anymore. Thx.

React.render((
  <Router history={history} intl={intlData} >
    <Route name="/" path="/" component={LoginScreen}>
      <Route name="login" path="login" component={LoginScreen}/>
      <Route name="settings" path="settings" component={SettingsScreen}/>
    </Route>
  </Router>
), document.body);```

@pbreah
Copy link

pbreah commented Aug 28, 2015

@garbin could you share the part where you integrate the IntlMixin mixin into the extended Router class, and where you set this.props.intl?

I would like to test your code with these details. The purpose is to centralize the calls to get the locale messages & formats from the router component so that all child components can access react-intl from the router, instead of adding a mixin on each child component.

Thanks,

@garbin
Copy link

garbin commented Aug 28, 2015

Yes, It's dead simple:). just two steps, take it.

1.create a Component through React.createClass(), Mixins only works here.

var BaseComponent = React.createClass({
    mixins:[ReactIntl.IntlMixin, ...your other mixin],
    ...OTHER MEMBERS
});

2.All of your component extends from BaseComponent, It'll be work.

class YourComponent extends BaseComponent{
   ...
}

This is not perfect, but it works!

On Sat, Aug 29, 2015 at 2:17 AM, pbreah notifications@github.com wrote:

@garbin https://github.com/garbin could you share the part where you
integrate the IntlMixin mixin into the extended Router class, and where you
set this.props.intl?

I would like to test your code with these details. The purpose is to
centralize the calls to get the locale messages & formats from the router
component so that all child components can access react-intl from the
router, instead of adding a mixin on each child component.

Thanks,


Reply to this email directly or view it on GitHub
#484 (comment).

@pbreah
Copy link

pbreah commented Aug 29, 2015

@garbin thanks for your quick response.

Merging these with the mixin feature and class inheritance is a good way to solve it, but we end up with two parent classes:

import React from 'react';
import { history } from 'react-router/lib/BrowserHistory';
import {Router as ReactRouter, Route} from 'react-router';
import AsyncProps from 'react-router/lib/experimental/AsyncProps';
import ReactIntl from 'react-intl';

var rootRoute = {
    path: '/',
    childRoutes: [
        require('./routes/path1'),
        require('./routes/path2')
    ],
    component: require('./components/App')
};

// here is the mixin that adds IntlMixin
var BaseComponent = React.createClass({
    mixins:[ReactIntl.IntlMixin]
});
// this similar to your example above
class Router extends ReactRouter {
    static get childContextTypes(){
        return Object.assign(super.childContextTypes, {
            locales: React.PropTypes.oneOfType([
                React.PropTypes.string,
                React.PropTypes.array
            ]),
            formats : React.PropTypes.object,
            messages: React.PropTypes.object,
            getmsg: React.PropTypes.func
        });
    }
    getChildContext() {
        var i18nLoader = require('i18n');
        var context = super.getChildContext() || {};

        context.locales = this.props.locales || ['en'];
        context.formats = this.props.formats || {};
        context.messages= this.props.messages|| {};
        return context;
    }
}
// Now need to inherit from Router and BaseComponent
// ran out of options as there is no multiple inheritance:

class BaseRouter extends Router {
   // BaseComponent??
}

React.render((
    <BaseRouter
        children={rootRoute}
        history={history}
        createElement={AsyncProps.createElement}
    />
), document.getElementById('main-content'));

Any ideas how to approach this? how did you do your final implementation on your code with a centralized react-intl?

Thanks,

@garbin
Copy link

garbin commented Aug 29, 2015

@pbreah The component I mentioned above is Component that will be used in <Route component={Component} /> , Not Router.

@apreg
Copy link

apreg commented Aug 29, 2015

Here is what I did. I know it 's a different approach but maybe someone can get something out of it. There is this react-intl-es6 repo. I've extended Intl like this in my App component:

var locale = navigator.language.split('-')
locale = locale[0];

var strings = messages[locale] ? messages[locale] : messages['de']
strings = Object.assign(messages['de'], strings);

var intlData = {
  locales: ['de', 'en'],
  messages: strings
};

export default
class App extends Intl {

  static propTypes = {
    children: PropTypes.object
  };
/*
  static contextTypes = {
    intl: PropTypes.object
  };
*/
  constructor() {
    super( intlData.locales, intlData.messages )
  }

  render() {
    return (
      <span>
      {React.addons.cloneWithProps(this.props.children)}
      </span>
    )
  }
}

then in app.js

React.render((
  <Router  history={ new HashHistory()}>
    <Route name="/" path="/" component={App}>
      <Route name="login" path="login" component={LoginScreen}/>
      <Route name="settings" path="settings" component={SettingsScreen}/>
    </Route>
  </Router>
), document.body);

and in LoginScreen.js

  static contextTypes = {
    intl: React.PropTypes.object
  };

...
{this.context.intl.getMessage('login')}

@pbreah
Copy link

pbreah commented Aug 29, 2015

@garbin and @apreg thanks for helping.

I finally got it working. I had to integrate it on the context (this.context) on the App component, then passed this to the children using a hack to propagate the "context" as custom props to the child components on the render method (for anyone that needs it):

// hack until React 0.14 is released.
    var bodyComponent = React.Children.map(this.props.children, function(child) {
      return React.cloneElement(child, {
        locales: this.props.locales,
        messages: this.props.messages
      });
    }.bind(this));

@apreg
Copy link

apreg commented Aug 30, 2015

Yeah, I guess that's what {React.addons.cloneWithProps(this.props.children)} is for in my code but mine is outdated, I know.

@jonaswindey
Copy link

@apreg , I'm trying to get it working on my side too but I'm having an issue with react-intl-es6

I get the following error:
Uncaught TypeError: Super expression must either be null or a function, not undefined

When I want to do:
export default class Root extends Intl {

Also

import {Intl} from 'react-intl-es6'
console.log(Intl) -> gives me undefined

Do you have an idea?
I'm using react-router 1.0.0 b3

@apreg
Copy link

apreg commented Sep 1, 2015

@jonaswindey You get undefined Intl probably because it is not exported. To solve this I added these few lines to node_modules/react-intl-es6/react-intl.js:

var _Intl = require('./Intl');
var _Intl2 = _interopRequireDefault(_Intl);
var _IntlApi = require('./IntlApi');
var _IntlApi2 = _interopRequireDefault(_IntlApi);
exports.Intl = _Intl2['default'];
exports.IntlApi = _IntlApi2['default'];

@jonaswindey
Copy link

Hm, thanks for the tip. Seems a bit dirty though (and every npm install will overwrite this)
Strange that we can't create issues on react-intl-es6

@apreg
Copy link

apreg commented Sep 1, 2015

Yeah, I agree. Maybe a pull request would be appropriate in this case but I'm too lazy right now :)

jonaswindey referenced this issue in AlexJozwicki/react-intl-es6 Sep 1, 2015
@apreg
Copy link

apreg commented Sep 1, 2015

@jonaswindey thx 👍

@donnrri
Copy link

donnrri commented Oct 1, 2015

Hi All

I have found a good solution to be to transferring props to children as in
{React.cloneElement(this.props.children, {someExtraProp: something })}

see : https://github.com/rackt/react-router/blob/master/UPGRADE_GUIDE.md

so my code is something like

function renderApp(i18n) {

var React = require('react');
var ReactRouter = require('react-router');
var Router = ReactRouter.Router;

var App = React.createClass({

render: function () {

  return (
    <AppShell>

        {React.cloneElement(this.props.children, {messages: i18n.messages , locales: i18n.locales})}

    </AppShell>
  );
}

});

var rootRoute = {
component: 'div',
childRoutes: [{
path: '/',
component: {App},
///etc
}]
};

React.render(
,
document.body
);

}
renderApp(////pass in locales/messages);

now I can access my required locales / messages in any child component of loaded view.

@donnrri
Copy link

donnrri commented Oct 1, 2015

Tha should be
React.render(
,
document.body
);

@dinodsaurus
Copy link

I canot find a working example of implementing format.js, I browsed thru all the suggestions but I canot find a one that is working, could someone please give a working gist or something ?

@davidshift
Copy link

+1

@taion
Copy link
Contributor

taion commented Oct 15, 2015

Wrap your Router in a container. This works with React v0.14 context. Please don't "+1" closed issues with unrelated comments unless you have something to add.

@davidshift
Copy link

@taion got it thanks for the response!

@slimjim777
Copy link

To re-render the app for a different locale, a container is recommended (https://facebook.github.io/react/blog/2015/10/01/react-render-and-top-level-api.html). An example of the approach for React 0.14.*:

// Translated messages
var Messages = require('./components/messages');

// Add the locales we need
addLocaleData(en);
addLocaleData(zh);

window.AppState = {
  container: document.getElementById("main"),

  getLocale: function() {
    return localStorage.getItem('locale') || 'en';
  },

  setLocale: function(lang) {
    localStorage.setItem('locale', lang);
  },

  render: function() {
    var locale = this.getLocale();

    ReactDOM.render((
      <IntlProvider locale={locale} messages={Messages[locale]}>
        <Router history={browserHistory}>
       ...
        </Router>
      </IntlProvider>
    ), this.container);
  },

  unmount: function() {
    ReactDOM.unmountComponentAtNode(this.container);
  },

  rerender: function() {
    this.unmount();
    this.render();
  }
}

window.AppState.render();

Then the locale changer component can call:

window.AppState.setLocale('zh');
window.AppState.rerender();

@KingWu
Copy link

KingWu commented May 17, 2016

@slimjim777 When i call rerender, i got the following warning. How can avoid it?

warning.js:44 Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the undefined component.

@slimjim777
Copy link

@KingWu You could try wrapping the call with a check for isMounted().

@lock lock bot locked as resolved and limited conversation to collaborators Jan 22, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests