Im using withTranslation with React Navigation and would like to set the headerTitle using static navigationOptions, but when using the withTranslation static methods are not copied, should withTranslation copy them with something like react hoist statics instead?
the uncool about https://github.com/mridgway/hoist-non-react-statics is the added 3kB to just hoist some statics that could be manually hoisted (as it's only one static in most cases).
Or just:
import React, { Component } from 'react';
import { withTranslation } from 'react-i18next';
import hoistStatics from 'hoist-non-react-statics';
class MyComponent extends Component {
static ...
}
export default hoistStatics(withTranslation(MyComponent), MyComponent);
True! Thanks :)
I would be happier if this was done in react-18next. It is very surprising and took me hours to figure out why stuff broke after upgrading to v10. We were using @withTranslations decorator previously, and it handled this.. that boilerplate is terrible, to have to put in all our class components!
@bigfish build your own: https://github.com/i18next/react-i18next/blob/master/src/withTranslation.js
currently, i got mixed feelings about adding this back -> adds more code that is not used by the majority. That hoist-non-react-statics is just not a small addin -> it's a 30% size of react-i18next
guess statics are only used on page level components - so that are not 100 of components.
Example with react-navigation
import React from 'react'
import { useTranslation } from 'react-i18next'
// NOTE: Need to copy static for react-naivation
// When translate headerTitles
export default function withTranslation(ns) {
return function Extend(WrappedComponent) {
function I18nextWithTranslation(props) {
const [t, i18n, ready] = useTranslation(ns, props)
return React.createElement(WrappedComponent, {
...props,
t,
i18n,
tReady: ready,
})
}
I18nextWithTranslation.navigationOptions = WrappedComponent.navigationOptions
return I18nextWithTranslation
}
}
Tough call. I personally try to avoid adding dependencies to my public npm packages if I can. Perhaps adding a note about manually hoisting statics to the docs would be sufficient?
Was the plan...just not yet had time to add that
Well, this sucks.
guess statics are only used on page level components - so that are not 100 of components.
You don't know my life. 馃槃
For real though, this has been a pita and a time drain. Is there anything we can do here?
@RWOverdijk why not just create either a wrapper in your code or just copy code from https://github.com/i18next/react-i18next/blob/master/src/withTranslation.js and add the hoist thingy on your code base
It works, sure, but a popular library breaking features from another popular library because of 3kb of code (which can also just be inlined) is a bit odd I think.
It's not a huge deal of course, I just felt like I had to add to it 馃槃 It's frustrating to lose time over this (even though it's in the docs).
I need to get something clear about this.
It seems that the static function getDerivedStateFromProps is automatically hoisted, but I need to hoist NavigationOptions manually. Is this correct?
@msageryd nothing is hoisted automatically / magically
I hear you, but I still don't understand.
This is what I do:
const Translated = withTranslation()(_DashboardView);
Translated.navigationOptions = _DashboardView.navigationOptions;
export default Translated;
How does getDerivedStateFromProps work? Seems magical to me.
This will give an error:
const Translated = withTranslation()(_DashboardView);
Translated.navigationOptions = _DashboardView.navigationOptions;
Translated.getDerivedStateFromProps = _DashboardView.getDerivedStateFromProps;
export default Translated;
getDerivedStateFromProps guess react takes care of that...as also https://github.com/mridgway/hoist-non-react-statics/blob/master/src/index.js#L7 treats them separately (respective excludes them from hoisting)
Thanks, as I suspected - magic =)
@msageryd A little explanations on this magic, I hope You find it helpful.
TL;DR;
The delivery service (React rendering pipeline) has some information on the package that you want to be delivered: it's fragile-ness, weight, the destination address etc (thats static propTypes, static getDerivedStateFromProps, componentDidMount() etc). You gift-wrapped it into opaque wrapping (withTranslation HoC), so nobody would be able to see what's inside, only the size and, maybe, general shape of it. If you want the recipient of the package (navigation library in your case) to see/know what's inside (navigationOptions here) without unwrapping the package, you must either make the wrapping translucent (withTranslation HoC must hoist/forward the thingies you may or may not want to be public), or write the details on the wrapping itself (i.e. do the hoisting manually).
More technical examples:
Hoisting is required only to the statics, that You, as a developer intend to use from outside of the component - it's public API if you want to call it that.
// SomeScreen.jsx/tsx
class SomeScreen ... {
// public API, you would use it somewhere else except the current file
static ROUTE_NAME = 'some_route';
// public API, navigation library will try to use it, but it knows nothing about
// the original SomeScreen component, it only receives the already wrapped
// into HoCs version of it
static navigationOptions = { ... };
// an internal React API, would be used by React, which accesses it directly,
// so it always *is* accessible for React
static propTypes = { ... };
// also an internal React API, used by the current *instance* of the
// SomeScreen component to update its own state, so, as it *is* always
// available to itself, it does not require the hoisting
// (not quite the exact and accurate description of the process I admit, but close enough)
static getDerivedStateFromProps() {
...
}
}
export default wrapIntoAHoC(SomeScreen);
// some_other_file.js/ts
import SomeScreen from ...;
...
const someScreenNavigator = createNavigator({
// usage of public API of your component, except SomeScreen in this case
// would not be an original SomeScreen component, but HoC-wrapped version of it
[SomeScreen.ROUTE_NAME]: SomeScreen,
});
// NotificationBus.jsx
class NotificationBus ... {
static notify(...) {} // public API
}
// some_another_file.js
if (notifyUser) {
NotificationBus.notify(...); // usage of public API of your component
}
// CustomInput.jsx
class CustomInput ... {
focus() {} // public API
}
// SomeAnotherComponent.js
somethingHappened() {
...
if (shoudFocusAnInput && this.customInputRef) {
// usage of the public API of your component. If the input component was wrapped
// into a HoC, you'll receive an exception here if that HoC doesn't forward the ref
this.customInputRef.focus();
}
}
When you wrap your component into a HoC, you effectively hide it's public API from the outside world.
The common approach and the general rule of thumb to avoid this issue is to support public API forwarding by the HoC itself. For static methods/and properties (_and only for them_) it is usually done with statics hoisting inside HoCs with the help of the hoisting utility library mentioned in the posts above. For the instance methods/properties (like with the inputRef.focus() example) the solution is to use ref forwarding with the React.forwardRef() utility function, that is part of the React core functionality.
_Given that React team leans more and more from components usage to pure render functions usage, it is understandable, why ref forwarding is a part of the library core and the statics forwarding isn't._
Here's my workaround function (in TypeScript), which calls the original withTranslation function (to get all its latest functionality) and wraps it in hoistNonReactStatics. (The messy explicit function typing was copied from react-i18next types to ensure compatibility.)
/* eslint-disable no-restricted-imports */
import {
withTranslation as originalWithTranslation,
Namespace,
WithTranslation,
WithTranslationProps,
} from 'react-i18next';
import hoistNonReactStatics from 'hoist-non-react-statics';
// Replacement for react-i18next 'withTranslation' HOC function due to
// https://github.com/i18next/react-i18next/issues/736
export function withTranslation(
ns?: Namespace,
options?: {
withRef?: boolean;
}
): <P extends WithTranslation>(
component: React.ComponentType<P>
) => React.ComponentType<Omit<P, keyof WithTranslation> & WithTranslationProps> {
const returnFunction = originalWithTranslation(ns, options);
return <P extends WithTranslation>(WrappedComponent: React.ComponentType<P>) => {
const TranslatedComponent = returnFunction(WrappedComponent);
return hoistNonReactStatics(TranslatedComponent, WrappedComponent);
};
}
The problem is, VSCode still suggests importing withTranslation from react-i18next and it would still accidentally be used later on. Therefore I added also an ESLint rule which forbids importing withTranslation from react-i18next:
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'react-i18next',
importNames: ['withTranslation'],
message: 'Use withTranslation from src/utils/l10n instead',
},
],
},
],
Personally I think that a library like react-i18next should 'just work'. The fact that it doesn't work together with another major library (react-navigation) is a major pita.
Most helpful comment
It works, sure, but a popular library breaking features from another popular library because of 3kb of code (which can also just be inlined) is a bit odd I think.
It's not a huge deal of course, I just felt like I had to add to it 馃槃 It's frustrating to lose time over this (even though it's in the docs).