Skip to content

Use React's new Context API #406

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

Merged
merged 14 commits into from
Jan 10, 2020
Merged

Conversation

Gregoor
Copy link
Contributor

@Gregoor Gregoor commented Jul 24, 2019

After talking with Stas it sounded like all of our internal projects are using a recent enough version of React to allow us to switch from the legacy to the new context API. My motivation is to add hooks to fluent. That'd follow in a later PR.

Functionality wise everything should be there for Localized, working at fixing the tests for it next. After that I'll look at withLocalization.

This would also include an API change, namely not exporting ReactLocalization anymore. With hooks it was possible to merge the same functionality into LocalizationProvider without it getting noisy (at least not to me)

@Gregoor
Copy link
Contributor Author

Gregoor commented Jul 31, 2019

Went with using jest after all. Generation of the snapshot tests was super easy, I've opted for inline mode as that makes it easier to see the expected results (and easier to compare before/after). Renamed the files according to jest's convention as well. Unfortunately that led to git losing track of some 🙈
Another downside is that Jest's snapshot output what the HTML tree looks like and there are some tests in here that check whether Fragment's are retained. Haven't yet figured out how to make those tests meaningful again.

Taking another week-or-so break from this, but if the general Jest direction is alright, I'd continue with that.

cc @stasm

@stasm
Copy link
Contributor

stasm commented Jul 31, 2019

Oh, this is great!

Another downside is that Jest's snapshot output what the HTML tree looks like and there are some tests in here that check whether Fragment's are retained. Haven't yet figured out how to make those tests meaningful again.

I added the Fragment tests recently to make sure they can be consumed by Localized. I don't think we need to ensure they're retained after render. I'd be OK with testing the HTML tree instead.

Taking another week-or-so break from this, but if the general Jest direction is alright, I'd continue with that.

I'm 100% for this change. Thank you!

@stasm
Copy link
Contributor

stasm commented Jul 31, 2019

Also, sorry about the conflicts your branch has against master now 😔 I hope they're not too hard to resolve.

@Gregoor
Copy link
Contributor Author

Gregoor commented Aug 1, 2019

Cool! :)

No worries about the conflicts, that's what happens when I create monolithic PRs against an active project 😁

@Gregoor
Copy link
Contributor Author

Gregoor commented Aug 6, 2019

alrighty, synced it up with master and updated withLocalization (+ its tests) to use the new context API as well.

I'm wondering if the provider_change and provider_context tests are worth updating, but that's related to my ideology being "only write tests for your API, not your internals", and in this case they're testing things that are already covered by the localized tests. If someone disagrees, I can also tackle those in my next session.

Apart from that outstanding todos are:

  • fix linting issues (ignored those so far)
  • make sure the project wide test commands run jest
    ...and I think that'd be it.

@Gregoor
Copy link
Contributor Author

Gregoor commented Aug 7, 2019

I might need some help on modifiying common.mk. For fluent-react it would just need to run jest --collect-coverage, but my bash scripting skills are not immediately up for the task.

@Pike
Copy link
Contributor

Pike commented Aug 7, 2019

I might need some help on modifiying common.mk. For fluent-react it would just need to run jest --collect-coverage, but my bash scripting skills are not immediately up for the task.

I'd look into make and not bash for this, something like https://www.gnu.org/software/make/manual/html_node/Overriding-Makefiles.html sounds interesting. Or just try to overwrite test:.

@stasm
Copy link
Contributor

stasm commented Aug 8, 2019

I think make doesn't allow overwriting targets. That's why in common.mk there's a check to decide which version of test to run: with or without test/index.js which sets things up if needed:

fluent.js/common.mk

Lines 22 to 23 in 4312437

test:
ifneq (,$(wildcard ./test/index.js))

Thinking about it, I probably got ahead of myself here. We only have 8 packages. It shouldn't be a lot of work to just move the test target to each package's Makefile, and it will allow much more flexibility when testing.

@stasm
Copy link
Contributor

stasm commented Aug 8, 2019

I'm wondering if the provider_change and provider_context tests are worth updating, but that's related to my ideology being "only write tests for your API, not your internals", and in this case they're testing things that are already covered by the localized tests. If someone disagrees, I can also tackle those in my next session.

I think it's fine to remove them.

@Gregoor
Copy link
Contributor Author

Gregoor commented Aug 12, 2019

Cool! 👌

I made the makefile change, but I'm not the most experienced makefile-engineer, so maybe someone wants to double-check what I did there.
Edit: Oh I think it was possible to overwrite tasks after all, so should I do that instead of moving tests into all the makefiles?

Had a bit of a change of mind wrt including the hook with this change: If people have to upgrade to a React-hook version to use this anyway, maybe they should be rewarded with a hook. Wdyt? Or should I rewrite this to not use hooks anyway? My thinking is that an upgrade inside of React 16 is painless (no breaking changes), so we might as well skip < 16.8

const { l10n, parseMarkup } = useContext(FluentContext);

if (l10n === null) {
throw new Error(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe log a warning and return child

Copy link
Contributor

@stasm stasm Dec 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing an error here changes the current behavior, which is to return the child element as the fallback, as @dvdmgl says. IIRC the purpose of not throwing was to make it easier to test code which uses <Localized> without having to set up a provider. I'm open to revisiting this but I think it would be safer to do it in a separate PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see another check for !l10n down below in line 70. I'd say this one up here is redundant then.

@stasm
Copy link
Contributor

stasm commented Nov 25, 2019

I'm back from my leave of absence, and I'd like to help land this PR. Thanks again to @Gregoor for submitting this PR and to @dvdmgl for the comments.

@Gregoor Would you be available and would like to finish this PR?

Had a bit of a change of mind wrt including the hook with this change: If people have to upgrade to a React-hook version to use this anyway, maybe they should be rewarded with a hook. Wdyt? Or should I rewrite this to not use hooks anyway? My thinking is that an upgrade inside of React 16 is painless (no breaking changes), so we might as well skip < 16.8

I think the use of hooks in this PR is fine. I was wodering if it would make sense to use the Context API verbatim, and even let users wrap their apps in <LocalizationContext.Provider> but if I understand correctly, we still need the custom LocalizationProvider to perform all the initialization of the context value.

When you say rewarded with a hook, do you mean something like a useLocalization hook provided by fluent-react? I'd like to do this for sure, but I'd prefer to do it in another PR to keep the scope of this one small.

@Gregoor
Copy link
Contributor Author

Gregoor commented Nov 26, 2019

Welcome back, Staś! Good to have you back, hope your time off was as great as it looked on Twitter :)
Happy to continue work on this, I think longer-term MDN will switch to Fluent so I think this is justified.

When you say rewarded with a hook, do you mean something like a useLocalization hook provided by fluent-react? I'd like to do this for sure, but I'd prefer to do it in another PR to keep the scope of this one small.

Huh, from reading my comment it seems that's what I meant but I can't say that I would still be able to argue for it 😁

@Gregoor
Copy link
Contributor Author

Gregoor commented Dec 4, 2019

@stasm I'm wondering what's left to be done here. From reading the comments, I think this was the only one outstanding from my side:

I made the makefile change, but I'm not the most experienced makefile-engineer, so maybe someone wants to double-check what I did there.
Edit: Oh I think it was possible to overwrite tasks after all, so should I do that instead of moving tests into all the makefiles?

@stasm
Copy link
Contributor

stasm commented Dec 5, 2019

Moving the test target into individual packages' Makefiles sounds good to me. In fact, I did the same in #436. I might take a closer look at common.mk in the near future; it's not as helpful as I though it would be.

I'll start reviewing this today, thanks @Gregoor!

Copy link
Contributor

@stasm stasm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, this is very close, but I'd like to make a few small changes before merging. My comments are below.

const { l10n, parseMarkup } = useContext(FluentContext);

if (l10n === null) {
throw new Error(
Copy link
Contributor

@stasm stasm Dec 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing an error here changes the current behavior, which is to return the child element as the fallback, as @dvdmgl says. IIRC the purpose of not throwing was to make it easier to test code which uses <Localized> without having to set up a provider. I'm open to revisiting this but I think it would be safer to do it in a separate PR.

const { l10n, parseMarkup } = useContext(FluentContext);

if (l10n === null) {
throw new Error(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see another check for !l10n down below in line 70. I'd say this one up here is redundant then.

@@ -0,0 +1,15 @@
module.exports = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file required for running tests with Jest? Please add a one-line comment explaining why we need it :)

}

componentWillReceiveProps(next) {
const { bundles } = next;
const bundles = useMemo(() => CachedSyncIterable.from(props.bundles), [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned about the use of useMemo here. To quote the docs:

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.

I'd prefer to use an API with stronger guarantees here, e.g. useEffect. Or, as you suggested on Slack, refactor this to memoize the entire context object.

]);
const contextValue = useMemo(
() => {
const l10n = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I preferred it when this was a separate ReactLocalization class in its own file. What do you think? I see that right now you're relying on a closure to have access to bundles; I'd OK with re-creating the instance of ReactLocalization when the props to the provider change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be in line with your proposal from Slack to memoize the entire context rather than individual props, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to (and now did) memoize the whole Provider component and not just the contextValue. That should make things a lot clearer. I also added a test to make sure that it stop unnecessary re-renders. It doesn't have the drawbacks of useMemo, namely

  • no extra per-render array+function-scope allocations
  • (probably) no future react might forget your memoization (at least not according to the docs)

A memoized (aka pure) component does an additional thing which a non-memoized Component doesn't do: shallowly compare props. But that's cheap enough and seems worth it, compared to (re)iterating the bundles.

Wrt the ReactLocalization class: I already had a comment typed out protesting against it, but then I gave myself a real think about it and ended up agreeing and bringing it back. Apparently I also removed ReactLocalization's comments in the process without finding a better place for them. Shame on me!
For posterity reasons why I think bringing it back is a good idea:

  • JS engines do a better job at optimizing for classes with known methods and fields
  • the concerns of the Provider and the Localization are separate enough
  • context is initialized with the same type as it's being used with (which I failed to do before)

And for completeness-sake a reason against it would've been that it moves related code further apart, making it potentially a little harder to grok and edit.

Thanks for the thorough review!

)
);
}
function WithDisplay(props) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for this rename?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly have no clue what happened there. Looks like an accident, I reverted it. Thanks for catching it!

Gregoor and others added 3 commits December 11, 2019 18:00
It simplifies the code a lot and might also make it faster, as memoization during render comes at the cost of additional array and function scope allocation.

This also adds a test to check that the Provider is not re-rendered when the bundles prop does not change.
Co-Authored-By: Staś Małolepszy <[email protected]>
@Gregoor Gregoor changed the title WIP Use React's new Context API Use React's new Context API Dec 11, 2019
Copy link
Contributor

@stasm stasm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this looks great. I have one more comment and a question, and we're done :)

  • I'd prefer to not add new Babel plugins, in this case @babel/plugin-proposal-class-properties. I'm on a quest to simplify the build setup across all Fluent packages. I hope to be able to remove @babel/plugin-proposal-async-generator-functions, too, when we drop Node.js 8 support in Require Node.js 10 #341.
  • I have a question below about what the value of the context should be. Right now it's {l10n, parseMarkup}, but I wouldn't mind moving parseMarkup into ReactLocalization, similar to reportError, and using the instance of ReactLocalization as the context value. What do you think?

FluentContext.Provider,
{ value: {
l10n: new ReactLocalization(props.bundles, props.parseMarkup),
parseMarkup: props.parseMarkup || createParseMarkup()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it stands right now, the ReactLocalization constructor doesn't take parseMarkup as an argument. That said, I wonder if it would actually be a good idea to make the value of the context be the instance of ReactLocalization, rather than the {l10n, parseMarkup} object. Then, passing parseMarkup to the constructor would be justified. @Gregoor, what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this one yet, as I don't see a semantic reason for them to live together. Are there other ergonomic/performance considerations that might make us want to do that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm mostly basing this on how this works in @fluent/dom. There we have the DOMLocalization class which has all the logic for localizing DOM elements. I'd lean towards considering parseMarkup part of ReactLocalization, too.

In longer term, I expect parseMakrup to be removed as a configurable property, which would leave l10n as the only property of the context value. I don't see many benefits to keeping this extra level of nesting, but perhaps it's because I generally tend to prefer flatter structures :)

@stasm
Copy link
Contributor

stasm commented Jan 10, 2020

I'd like to change the value of the context to be the instance of ReactLocalization, but I'd also like to land this PR :) @Gregoor, any objections to landing it as-is? I'll open a new PR to consider the context value change.

@Gregoor
Copy link
Contributor Author

Gregoor commented Jan 10, 2020

I'd like to change the value of the context to be the instance of ReactLocalization, but I'd also like to land this PR :) @Gregoor, any objections to landing it as-is? I'll open a new PR to consider the context value change.

I remember typing a reply before the holidays and having some thoughts about this (agreeing with you), but I didn't seem to have send it 🤔 I'm happy either way!

Copy link
Contributor

@stasm stasm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, thanks. Let's land this as-is, and I'll file a follow-up about the context value.

@stasm stasm merged commit 0bc8a06 into projectfluent:master Jan 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants