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

feat(v2): add support another Prism theme for dark mode #1984

Merged
merged 5 commits into from
Dec 25, 2019

Conversation

lex111
Copy link
Contributor

@lex111 lex111 commented Nov 13, 2019

Motivation

I think it's worth considering the introduction of a separate Prism theme for dark mode.

This is proof of concept (I'm not sure about peft of this solution) and there is a issue with how to switch the Prism theme when changing dark mode toggle in header. Any ideas?

Have you read the Contributing Guidelines on pull requests?

Yes

Test Plan

Config:

//...

  themeConfig: {
    prism: {
      darkTheme: require('prism-react-renderer/themes/dracula'),
    },

//...

@lex111 lex111 requested a review from yangshun November 13, 2019 10:37
@facebook-github-bot facebook-github-bot added the CLA Signed Signed Facebook CLA label Nov 13, 2019
@lex111 lex111 requested a review from endiliey November 13, 2019 10:37
@docusaurus-bot
Copy link
Contributor

docusaurus-bot commented Nov 13, 2019

Deploy preview for docusaurus-2 ready!

Built with commit 1cc3244

https://deploy-preview-1984--docusaurus-2.netlify.com

@docusaurus-bot
Copy link
Contributor

docusaurus-bot commented Nov 13, 2019

Deploy preview for docusaurus-preview ready!

Built with commit d694a29

https://deploy-preview-1984--docusaurus-preview.netlify.com

@endiliey
Copy link
Contributor

d there is a issue with how to switch the Prism theme when changing dark mode toggle in header. Any ideas?

maybe our hooks need to be fixed to make it more flexible.Maybe we should have something like mutation observer to listen to dom changes ? hmm. Not really sure

@lex111
Copy link
Contributor Author

lex111 commented Nov 14, 2019

@endiliey I added the simplest event bus, now when you change the theme in the navbar, the Prism theme also changes dynamically. Can you take a look the implementation please?

@endiliey
Copy link
Contributor

Maybe we can try with dogfooding it in netlify preview first, so that others could test it too.
You can disable the theme-live-codeblock temporarily, then set the required config, and so on. So we can test it netlify preview. I'm not really sure myself on the best approach to do this hehe

cc @yangshun and @wgao19

@wgao19
Copy link
Contributor

wgao19 commented Nov 14, 2019

how often will we use the event bus? unless we will pick it up in multiple places i feel it's a bit overkill. i think the observer hook may suffice

@lex111
Copy link
Contributor Author

lex111 commented Nov 14, 2019

Maybe we can try with dogfooding it in netlify preview first, so that others could test it too.

Makes sense, done ✔️

Perhaps in the future the event bus will be needed more, I can’t know for sure, if there is an alternative to the current solution, I only got the event bus in my mind.

i think the observer hook may suffice

Сan detail what you mean by that?

UPD: If you mean MutationObserver, then I'm not sure if this is a good solution, largely because then we rely on third-party dependency. Event bus is more transparent and understandable, IMO.

@lex111 lex111 changed the title feat(v2): add support another Prism theme for dark mode [WIP] feat(v2): add support another Prism theme for dark mode Nov 14, 2019
@wgao19
Copy link
Contributor

wgao19 commented Nov 20, 2019

MutationObserver is web API, no extra dependency needed.

I'm not against event bus just wondering if this is the only use case.

@endiliey
Copy link
Contributor

The downside of using this subscription method is that the theme selector has to dispatch when we change the theme. But generally that's okay because only 1 toggle though.

mutationobserver advantage is that it can just listen to data-theme DOM and is somewhat look less complex. Less line of codes ??

some random pseudocode that i made (might not work :

// CodeBlock.js
useEffect(() => {
    const observer = new MutationObserver(mutationsList => {
      for (let mutation of mutationsList) {
        if (mutation.attributeName !== 'data-theme') {
          continue;
        }
        const { theme } = document.documentElement.dataset;
        changePrismTheme(theme);
        return;
      }
    });
    observer.observe(document.documentElement, { attributes: true });
    return () => observer.disconnect();
  }, []);

@lex111
Copy link
Contributor Author

lex111 commented Nov 25, 2019

@endiliey thank you, this is a working solution, although the option with the event bus seems to me more understandable, then should I remove it and replace it with the MutationObserver?

@endiliey
Copy link
Contributor

endiliey commented Nov 25, 2019

Hmm im not sure. Both approach works well for me.

I just tried it locally like this,

// CodeBlock.js
  const [theme] = useTheme();
  const lightThemePrism = prism.theme || defaultTheme;
  const darkThemePrism = prism.darkTheme || defaultTheme;
  const [prismTheme, setPrismTheme] = useState(
    theme === 'dark' ? darkThemePrism : lightThemePrism,
  );

  useEffect(() => {
    const observer = new MutationObserver(mutationsList => {
      for (let mutation of mutationsList) {
        if (mutation.attributeName !== 'data-theme') {
          continue;
        }
        const {theme} = document.documentElement.dataset;

        setPrismTheme(theme === 'dark' ? darkThemePrism : lightThemePrism);
        return;
      }
    });
    observer.observe(document.documentElement, {attributes: true});

    return () => observer.disconnect();
  }, []);

And i was able to delete all the eventBus code & only CodeBlock.js need some code changes. It's much lesser code for now

@endiliey
Copy link
Contributor

cc @yangshun and @wgao19

I'm ok with any approach 😉

Copy link
Contributor

@endiliey endiliey left a comment

Choose a reason for hiding this comment

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

just gonna stamp. final call from other maintainers

@endiliey
Copy link
Contributor

endiliey commented Nov 28, 2019

I’d like to suggest not to merge this yet for now. Upon working on the no flash problem, its known that “document data-theme” should be the main source of truth of whether dark mode is on or not.

This one requires toggle to dispatch an event, vanilla javascript cant dispatch a react event. Lets put this on hold till we solve the no flash problem (at least) and make sure it can work as well

};

const useEventBus = (type, listener, deps = []) => {
useEffect(() => subscribe(type, listener), deps);
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we can return unsubscribe method here for better cleanup?

useEffect(() => {
  subscribe(type, listener);
  return () => subscribers[type] = subscribers[type].filter(fn => fn !== listener);
}, deps);

@endiliey endiliey added status: don't merge yet This PR is completed, but we are still waiting for other things to be ready. pr: new feature This PR adds a new API or behavior. labels Nov 28, 2019
Copy link
Contributor

@yangshun yangshun left a comment

Choose a reason for hiding this comment

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

I might be missing something, but I think the event bus is not necessary, please refer to inline comments.

Not really in favor of event bus/emitter-style of notifications. It feels like we are back to the pre-React days where we have messy event handlers sprinkled throughout the app.

In the worst case, how about we add the theme into the context and listen to the props?

e => {
const newTheme = e.target.checked ? 'dark' : '';
setTheme(newTheme);
dispatch('docusaurus-change-theme', {newTheme});
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this dispatch() even needed? The useTheme hook stores the theme and when it gets updated, them components which are using the value will also re-render. setTheme will already trigger an update for those components using useTheme. I think this is unnecessary and as a result, we probably don't need the event bus.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The useTheme hook stores the theme and when it gets updated, them components which are using the value will also re-render.

I tried go that way, but this method does not work for me.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'll patch and try it. Give me a few days, I'm moving countries today 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes please! Although it seems to me that this is not such a bad decision, but with hooks/context it not worked out for me ;(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@yangshun any news?

Copy link
Contributor

Choose a reason for hiding this comment

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

The useTheme hook stores the theme and when it gets updated, them components which are using the value will also re-render.

I was completely wrong (wonky memory). useTheme is state and local to a component. setTheme only updates the theme value within the component.

@yangshun
Copy link
Contributor

Sorry I haven't got to it. Gimme a while more. You might want to resolve the merge conflict first anyway.

Copy link
Contributor

@yangshun yangshun left a comment

Choose a reason for hiding this comment

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

I finally got the chance to take a good look at this. I apologize for the following:

  • For taking so long to get back to you. Work has been really hectic and I'm busy getting my last minute #impacc for the half.
  • My earlier comment that theme would automatically update was incorrect. useTheme would not sync state across components - I completely forgot that useTheme uses useState, and component state is local. I found out that by doing useTheme in so many components in our code, we were initializing theme state around 7 times on certain pages (once for each codeblock). That's completely unnecessary and unintended. The proper way was for theme to be part of context. Heck, even the React context examples used theme as their example.

I'm not in favor of the event bus as a communication mechanism because the theme value is clearly part of state and we should use an idiomatic React approach.

I considered a few approaches, including listening to the window.localStorage changes and syncing the theme state across components, but the window.addEventListener('storage', ...) API doesn't allow listening for events on the same page. Hence I settled on creating a theme context that can be used by every component which needs the theme.

The good:

  • Individual components now have control over the theme and can set the theme and update all places which are relying on that theme context/state.
  • No duplicated theme state across components. It's all the way at the top of the component tree in the theme context provider

The bad:

  • I had to inject the theme provider in <Layout/>. People who swizzled layout will not get the updates or their code might even break. Thankfully it's just a one-line change on their part and they can easily move to this theme context approach.
  • Users who create their own layout need to explicitly use the theme provider as well for theming functionality, whereas the event bus approach doesn't have to, as there is no central state (distributed across the components which use it).

I'm unable to push to your PR branch, so I made a new one on this repo and the changes can be found here. Let me know your thoughts!

@lex111
Copy link
Contributor Author

lex111 commented Dec 21, 2019

@yangshun thanks for the detailed analysis! I fully support these changes, especially since you increased performance with context? Indeed, it is much better and more understandable!
You only need to explicitly specify in release notes about manually adding theme context support to Layout if one of the users has swizzled this component.
In this case, I close this PR, will you open a new one?

@lex111 lex111 closed this Dec 21, 2019
@yangshun
Copy link
Contributor

yangshun commented Dec 21, 2019

You did the bulk of the work and I was only improving on your work. I think it would be ok for you to patch the commit on your existing branch and retain this PR for the discussion. Alternatively, you can create a new PR using my branch. I prefer that the feature to be attributed to you 😄

@lex111
Copy link
Contributor Author

lex111 commented Dec 21, 2019

@yangshun fine, I'll take care of the rest and complete PR :)

P.S. If you took the time to research a long-standing issue with code block, it would be very nice! 😃 (just friendly reminder, because it still bothers me)

@lex111 lex111 reopened this Dec 21, 2019
@popuguytheparrot
Copy link

Hi. When it will merged?

@yangshun
Copy link
Contributor

@lex111 I can take over this and get it merged if you allow me access to the branch/repo.

@lex111
Copy link
Contributor Author

lex111 commented Dec 25, 2019

@yangshun you got it. Sorry that I could not do it myself quickly.

@yangshun yangshun removed the status: don't merge yet This PR is completed, but we are still waiting for other things to be ready. label Dec 25, 2019
@yangshun yangshun merged commit 92c68ed into facebook:master Dec 25, 2019
@yangshun yangshun deleted the prism-dark-theme branch December 25, 2019 14:52
@yangshun
Copy link
Contributor

Finally merged this! I'll add docs and update our templates with this new feature in a follow-up PR.

@endiliey
Copy link
Contributor

endiliey commented Dec 26, 2019

I think its still buggy

If you go to https://v2.docusaurus.io/docs/installation or any link, set dark mode and refresh, it will still be light theme on first page reload.
bug

@popuguytheparrot
Copy link

i think need add in deps of onToggleChange theme var

@yangshun
Copy link
Contributor

Hmm thanks for catching. I'll look into it.

@yangshun
Copy link
Contributor

yangshun commented Dec 26, 2019

I think this only happens on the build version of the site. It works fine on development. The SSR step isn't theme-aware and always renders the light theme syntax highlighting to static HTML. The build step doesn't have access to the theme localStorage so I can only set the syntax highlighting after page load.

@yangshun
Copy link
Contributor

Root cause - React doesn't update styles that are rendered via SSR - facebook/react#11128

@endiliey
Copy link
Contributor

yes only in prod. I explained it before on my dark mode fouc fix

See #2069

isClient context is needed because we need to do two pass rendering
Why do we need two pass rendering ?
For full reasoning, please check the whole thread starting from this comment #2057 (comment)

@binarylogic
Copy link
Contributor

🎉 thanks for working on this, we're looking forward to this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed Signed Facebook CLA pr: new feature This PR adds a new API or behavior.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants