-
Notifications
You must be signed in to change notification settings - Fork 2k
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
i18n-calypso: add useTranslate hook #31043
Conversation
This is now ready for review. I did some optimizations to prevent creating unneeded objects on every hook call. Added section about the hook to README. And added a basic unit test. I'd also love to test the reactive rerenders of |
Sidenote, could this be relevant for Gutenberg? See WordPress/gutenberg#9846 specifically. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, great seeing hooks making their way into the codebase :) I left some comments for you to consider.
const onChange = () => setLocaleSlug( getLocaleSlug() ); | ||
i18n.on( 'change', onChange ); | ||
return () => i18n.off( 'change', onChange ); | ||
}, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The empty array is an important detail here to make sure that the effect is only applied on mount, and not on every update. Good work 👍
return () => i18n.off( 'change', onChange ); | ||
}, []); | ||
|
||
return [ translate, localeSlug ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I'd use an array here; the two properties aren't as closely-related as the [ property, setProperty ]
pair returned by useState
, and they likely won't need to be renamed either. I'd probably return an object instead.
Still, this is really a matter of personal taste here, so happy either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option is to just return translate
:
const translate = useTranslate();
Components never really use anything else. The localeSlug
needs to be part of state, because the translate
function is always the same and something needs to change to trigger a rerender. That will be especially true if we switch to React Context instead of one i18n
listener per each mounted localized component.
I returned array mainly because it's an existing convention for several hooks. But thinking about it a little more, it doesn't look like a good fit for this particular hook.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to just returning translate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to make the language slug available as translate.localeSlug
?
renderer.render( <Label /> ); | ||
|
||
// check that it's translated | ||
expect( renderer.getRenderOutput() ).toBe( 'háček (cs)' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mocking i18n
might allow you to test reactivity for this hook. Take a look at the react-helpers.js
test in https://github.com/Automattic/wp-calypso/pull/31081/files for a similar situation where I mocked a browser API.
const [ localeSlug, setLocaleSlug ] = React.useState( getLocaleSlug ); | ||
|
||
React.useEffect(() => { | ||
const onChange = () => setLocaleSlug( getLocaleSlug() ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran into some confusing issues in my own PR when dealing with subscriptions for multiple instances of the same component. This code looks good, but I'd suggest testing reactivity when you have multiple instances of the same component, and ensure they all get updated correctly.
const getLocaleSlug = i18n.getLocaleSlug.bind( i18n ); | ||
|
||
return function useTranslate() { | ||
const [ localeSlug, setLocaleSlug ] = React.useState( getLocaleSlug ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See https://reactjs.org/docs/hooks-reference.html#lazy-initial-state for what passing a function to useState means. TL;DR: it calls the function to generate the first state, only calling it on the first render
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's exactly what we need here, isn't it? The first render will initialize the state value, and all further changes come from the change
listener.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup! I just found it surprising, as I didn't know that useState could take an initializer function. Sorry I wasn't more clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my first comment should have read: "A note to anyone else reviewing this PR, see ..."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a great blog post that describes many possible gotchas: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
One source of subtle bugs is capturing a stale value inside a closure:
const [ count, setCount ] = useState( 0 );
useEffect( () => {
setTimeout( () => setCount( count + 1 ), 1000 );
}, [] );
Because of the []
argument, useEffect
will call the effect function only once, on mount, and the count
value will remain bound to 0
, even if the state changed in the meantime.
It's interesting that the setCount
function doesn't have this problem and can be saved without problems. It's assumed to be the same for every call. Ref objects returned from useRef()
can also be saved. I don't know what exactly the rules and guarantees are -- which values are safe and which are not?
I modified the hook to just return The As a next step, I'll look into @sgomes' suggestions on reactivity and its testing. |
Use lazy creation of the initial state, create the change handler inside the `useEffect` callback. Both changes prevent creation of unneeded function objects on every hook call.
…ose translate.localeSlug property
ff89b42
to
e1e4a93
Compare
e1e4a93
to
2cc873d
Compare
I added some unit tests that test reactive rerenders when the There was one subtle bug that I had to fix: Because the The solution is to create a new instance of the bound We should eventually switch to React Context that creates just one instance of the |
@ockham For a solution for the component interpolation problem specifically? Or for something else? This PR takes the I'd like to learn more about the differences between |
Yeah, for that. Sorry, I should've been clearer.
Yeah, it was mostly the React helper I thought might be a good fit for Gutenberg.
Yep, let's discuss that some time (maybe after we've finished moving the JP blocks code to the JP repo 😅 ) |
The second React hook in Calypso 🎉 (after #31035)
useTranslate
is a hook version of thelocalize
HOC that provides thetranslate
function and thelocaleSlug
string, nothing else.Adding also first use in
blocks/post-share/no-connections-notice
. See #31022 for instructions where to find it and how to test it.Still work in progress, need to add tests (I have no idea how to test hooks at this moment) and documentation.