morfeojs / morfeo

Morfeo is a build-time CSS-in-TS solution for the next level theming, with the minimum amount of shipped CSS.
https://morfeo.dev
MIT License
36 stars 4 forks source link

Use `useSyncExternalStore` to sync morfeo's theme with react #86

Closed mauroerta closed 2 years ago

mauroerta commented 3 years ago

Morfeo is born to work everywhere, it already works with React, Svelte, Angular or any other JS framework. Anyway for any of these framework we usually write packages to make the integration as good as possible, for example with directives for svelte or hook for react.

React v18 introduced a new hook called useSyncExternalStore, this hook helps to sync external states with the react state. It seems to be the perfect solution to always be sure that the theme used (and returned) by the hooks or @morfeo/hooks is the current used inside the morfeo instance.

It will probably unlock even other functionalities like:

References:

Before proceeding with this issue, we need to upgrade to React 18. The upgrade to React 18 is covered by this issue

mauroerta commented 2 years ago

Whenever the React team will release react@18.0.0 the possible implementation of this could be something like the following:

inside @morfeo/hooks package

import { morfeo, theme as themeHandler, ThemeName } from '@morfeo/core';
import { useSyncExternalStore } from 'react';

function subscribe(...callback: Parameters<typeof themeHandler.subscribe>) {
  const uid = themeHandler.subscribe(...callback);
  return () => themeHandler.cleanUp(uid);
}

export function useMorfeo() {
  const theme = useSyncExternalStore(subscribe, morfeo.getTheme);
  const current = morfeo.getCurrentTheme();

  function setCurrent(themeName: ThemeName) {
    morfeo.setCurrentTheme(themeName);
  }

  return { theme, current, setCurrent, subscribe };
}

To test if this code is working, I think this simple test would prove the correct behavior:

import { renderHook, act } from '@testing-library/react';
import { morfeo, Theme, ThemeName } from '@morfeo/core';
import { useMorfeo } from '../src/useMorfeo';

const LIGHT_THEME = {
  colors: {
    primary: 'black',
    secondary: 'white',
  },
} as Theme;

const DARK_THEME = {
  colors: {
    primary: 'white',
    secondary: 'black',
  },
} as Theme;

const LIGHT_THEME_KEY = 'light' as ThemeName;
const DARK_THEME_KEY = 'dark' as ThemeName;

beforeAll(() => {
  morfeo.setTheme(LIGHT_THEME_KEY, LIGHT_THEME);
  morfeo.setTheme(DARK_THEME_KEY, DARK_THEME);
  morfeo.setCurrentTheme(LIGHT_THEME_KEY);
});

describe('useMorfeo', () => {
  it('should return the theme', () => {
    const { result } = renderHook(() => useMorfeo());

    expect(result.current.theme).toEqual(LIGHT_THEME);
  });

  describe('when the theme changes', () => {
    it('should return the dark theme', () => {
      const { result } = renderHook(() => useMorfeo());

      act(() => {
        result.current.setCurrent(DARK_THEME_KEY);
      });

      expect(result.current.theme).toEqual(DARK_THEME);
    });
  });
});

In case everything works, we should also think about some other changes, for example:

MorfeoProvider

Currently, this provider is used to force a re-render when the theme changes, since useSyncExternalStore has been introduced exactly for this reason, we can remove MorfeoProvider and use the hook useMorfeo inside the other hooks (such as useTheme, useStyle and so on) to force a re-render when needed. Another option is to keep MorfeoProvider and use the hook useMorfeo (or at least useSyncExternalStore) only in the provider and leave all the other hooks as they are (no breaking changes at all)

mauroerta commented 2 years ago

🎉 React 18.0.0 is OUT 🎉

I already tried the snippet in the previous comment and seems to work.

Before opening a PR I'll wait for some other updates in other dependencies like @testing-librty/react, @types/react, and so on... Anyway, PRs are welcome if anyone wants to try to make this update!