From f5c15ca468ea74e3560104228ea63e1a99231329 Mon Sep 17 00:00:00 2001 From: mauroerta Date: Sun, 29 May 2022 22:47:41 +0200 Subject: [PATCH 1/4] BREAKING CHANGE: #86 use `useSyncExternalStore` to sync morfeo with the React render. Removed `MorfeoProvider` / `MorfeoContext` and introduced `useMorfeo` Closes #86 --- devtool/src/devtool/index.tsx | 8 +- docs/docs/Packages/hooks.mdx | 91 ++++++++++---- docs/docs/Packages/native.mdx | 4 +- examples/react/src/index.tsx | 6 +- packages/cli/tests/utils/run.ts | 6 - packages/hooks/README.md | 138 ++------------------- packages/hooks/src/MorfeoContext.tsx | 4 - packages/hooks/src/MorfeoProvider.tsx | 20 --- packages/hooks/src/index.ts | 3 +- packages/hooks/src/useCurrentTheme.ts | 20 +-- packages/hooks/src/useMorfeo.ts | 17 +++ packages/hooks/src/useProps.ts | 4 +- packages/hooks/src/useStyles.ts | 8 +- packages/hooks/src/useTheme.ts | 8 +- packages/hooks/tests/customRenderer.ts | 11 -- packages/hooks/tests/useMorfeo.test.ts | 46 +++++++ packages/hooks/tests/useProps.test.ts | 2 +- packages/hooks/tests/useStyles.test.ts | 31 +++-- packages/hooks/tests/useTheme.test.ts | 11 +- packages/react/tests/useClassName.test.tsx | 7 +- packages/react/tests/useStyles.test.tsx | 5 +- 21 files changed, 184 insertions(+), 266 deletions(-) delete mode 100644 packages/hooks/src/MorfeoContext.tsx delete mode 100644 packages/hooks/src/MorfeoProvider.tsx create mode 100644 packages/hooks/src/useMorfeo.ts delete mode 100644 packages/hooks/tests/customRenderer.ts create mode 100644 packages/hooks/tests/useMorfeo.test.ts diff --git a/devtool/src/devtool/index.tsx b/devtool/src/devtool/index.tsx index 2f399c55..0b51b0e1 100644 --- a/devtool/src/devtool/index.tsx +++ b/devtool/src/devtool/index.tsx @@ -1,6 +1,6 @@ import { createRoot } from 'react-dom/client'; import browser from 'webextension-polyfill'; -import { resetCss, MorfeoProvider } from '@morfeo/react'; +import { resetCss } from '@morfeo/react'; import { getThemeFromAppAndInitMorfeo } from '../_shared/utils'; import { MORFEO_DEVTOOL_PANEL_NAME, ASSETS_PATHS } from '../_shared/constants'; import Devtool from './Devtool'; @@ -19,9 +19,5 @@ const container = document.getElementById('devtool'); if (container) { const root = createRoot(container); - root.render( - - - , - ); + root.render(); } diff --git a/docs/docs/Packages/hooks.mdx b/docs/docs/Packages/hooks.mdx index 1fd642b0..c084b363 100644 --- a/docs/docs/Packages/hooks.mdx +++ b/docs/docs/Packages/hooks.mdx @@ -7,6 +7,10 @@ description: morfeo's hooks package **@morfeo/hooks** expose a set of hooks to easily use morfeo inside a `react` context. +:::info React v18 +To properly use Morfeo in a React environment, you need to have a version of `react >= 18` +::: + ## Installation ```bash @@ -17,7 +21,6 @@ npm i @morfeo/hooks yarn add @morfeo/hooks ``` -- [MorfeoProvider](#morfeo-provider) - [useTheme](#usetheme) - [useThemeSlice](#usethemeslice) - [useThemeValue](#usethemevalue) @@ -27,24 +30,9 @@ yarn add @morfeo/hooks Advanced +- [useMorfeo](#useMorfeo) - [useProps](#useprops) -## Morfeo Provider - -To sync morfeo with react you have to first of all wrap you app with the `MorfeoProvider`: - -```tsx -import { MorfeoProvider } from '@morfeo/hooks'; - -function App() { - return ( - - - - ); -} -``` - ## useTheme Use this hook to get the current theme and use it inside a components: @@ -118,9 +106,7 @@ they correspond to `morfeo.getCurrentTheme` and `morfeo.setCurrentTheme` of the ```jsx live function Button() { - // highlight-start const [currentTheme, setCurrentTheme] = useCurrentTheme(); - // highlight-end const style = useStyle({ componentName: 'Button', @@ -129,9 +115,7 @@ function Button() { }); const onClick = () => { - // highlight-start setCurrentTheme(currentTheme === 'light' ? 'dark' : 'light'); - // highlight-end }; return ( @@ -184,14 +168,73 @@ Just like useTheme, **useStyles** and **useStyle** are the equivalent of the [co but they force a re-render when the theme changes. ::: -## useProps +## Advanced + +### useMorfeo + +This hook is used under the hood in all the others hooks, you'll probably never need to use it directly. +It returns the same `morfeo instance` that you'll get from the core API, but it's reactive, to understand +the difference just look at these 2 examples: + +By using `useMorfeo` the component is subscribed to theme changes, which means that if the theme changes the component +will be re-rendered: + +```tsx live +function App() { + const morfeo = useMorfeo(); + + const onClick = () => { + morfeo.setCurrentTheme( + morfeo.getCurrentTheme() === 'light' ? 'dark' : 'light', + ); + }; + + return ( +

+ The current theme is: {morfeo.getCurrentTheme()} +
+ +

+ ); +} +``` + +If instead we use directly the `morfeo` instance from the core-api, the component will not re-render when the theme changes: + +```tsx live +function App() { + const onClick = () => { + morfeo.setCurrentTheme( + morfeo.getCurrentTheme() === 'light' ? 'dark' : 'light', + ); + }; + + return ( +

+ The current theme is: {morfeo.getCurrentTheme()} +
+ +

+ ); +} +``` + +### useProps Use it to get the default props of a components, a common use is to merge the defaut props with the current props: -```jsx +```jsx live function MyButton(props) { const defaultProps = useProps('Button', 'primary'); + const className = useClassName({ + componentName: 'Button', + variant: 'primary', + }); - return + ); } ``` diff --git a/docs/docs/Packages/native.mdx b/docs/docs/Packages/native.mdx index 42f3d24c..3860605e 100644 --- a/docs/docs/Packages/native.mdx +++ b/docs/docs/Packages/native.mdx @@ -5,7 +5,7 @@ title: native description: morfeo's native package --- -@morfeo/native package is brings to `React Native` the morfeo's eco system +@morfeo/native package brings to `React Native` the morfeo's ecosystem ## Installation @@ -23,7 +23,7 @@ yarn add @morfeo/native ## Usage -Just like @morfeo/react, the API of this package is the same as [@morfeo/hooks](./hooks.mdx), the main differences between `@morfeo/react` and `@morfeo/native` are related to how the parsers converts style objects to valid style for React Native, for example in @morfeo/native there you cannot use any pseudo element/class and shadows output is different (`shadowOffset`, `shadowColor`, `shadowOpacity` and `shadowRadius` instead of `boxShadow`) +Just like @morfeo/react, the API of this package is the same as [@morfeo/hooks](./hooks.mdx), the main differences between `@morfeo/react` and `@morfeo/native` are related to how the parsers converts style objects to valid style for React Native, for example in @morfeo/native you cannot use any pseudo element/class, and also, shadows output is different (`shadowOffset`, `shadowColor`, `shadowOpacity` and `shadowRadius` instead of `boxShadow`) ```tsx import { View, ViewProps } from 'react-native'; diff --git a/examples/react/src/index.tsx b/examples/react/src/index.tsx index 9dd775f2..897704f0 100644 --- a/examples/react/src/index.tsx +++ b/examples/react/src/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; import { initPreset } from '@morfeo/preset-default'; -import { morfeo, resetCss, loadFont, MorfeoProvider } from '@morfeo/react'; +import { morfeo, resetCss, loadFont } from '@morfeo/react'; import { enableMorfeoDevTool } from '@morfeo/dev-tools'; import App from './App'; import reportWebVitals from './reportWebVitals'; @@ -48,9 +48,7 @@ if (container) { const root = createRoot(container); root.render( - - - + , ); } diff --git a/packages/cli/tests/utils/run.ts b/packages/cli/tests/utils/run.ts index e8c3904c..355b2324 100644 --- a/packages/cli/tests/utils/run.ts +++ b/packages/cli/tests/utils/run.ts @@ -2,9 +2,6 @@ import { cli } from 'cli-ux'; import { morfeoCLI } from '../../src/commands'; export function run(...args: string[]) { - const _log = console.log; - const _error = console.error; - const _info = cli.info; const stdout: any[] = []; const stderr: any[] = []; @@ -12,21 +9,18 @@ export function run(...args: string[]) { if (typeof message === 'string') { stdout.push(message); } - return _info(message); }; console.log = (message: any) => { if (typeof message === 'string') { stdout.push(message); } - return _log(message); }; console.error = (message: any) => { if (typeof message === 'string') { stderr.push(message); } - return _error(message); }; morfeoCLI(args); diff --git a/packages/hooks/README.md b/packages/hooks/README.md index e0618695..202d1b6e 100644 --- a/packages/hooks/README.md +++ b/packages/hooks/README.md @@ -12,142 +12,20 @@ **@morfeo/hooks** expose a set of hooks to easily use morfeo inside a `react` context. +> **React v18** +> +> To properly use Morfeo in a React environment, you need to have a version of `react >= 18` + ## Installation ```bash # npm -npm i @morfeo/react +npm i @morfeo/hooks # yarn -yarn add @morfeo/react -``` - -- [MorfeoProvider](#morfeo-provider) -- [useTheme](#usetheme) - - [useThemeSlice](#usethemeslice) - - [useThemeValue](#usethemevalue) -- [useStyles](#usestyles) - - [useStyle](#usestyle) - -Advanced - -- [useProps](#useprops) - -## Morfeo Provider - -To sync morfeo with react you have to first of all wrap you app with the `MorfeoProvider`: - -```tsx -import { MorfeoProvider } from '@morfeo/hooks'; - -function App() { - return ( - - - - ); -} +yarn add @morfeo/hooks ``` -## useTheme - -Use this hook to get the current theme and use it inside a components: - -```jsx -const MyComponent: React.FC = () => { - const theme = useTheme(); - - return ( -
-

My primary color is: {theme.colors.primary}

-

My xxl space is: {theme.spaces.xxl}

-
- ); -}; -``` - -> If you already know the [core API](./core) useTheme is the equivalent of `morfeo.getTheme()`, the main difference is that useTheme react -> to theme changes and force a re-render of the component. - -Most of the time you don't need all theme, but just a slice or single value, in this cases [useThemeSlice](#useThemeSlice) and [useThemeValue](#useThemeValue) can will give you only the part of the theme you want: - -### useThemeSlice - -```jsx -const MyComponent: React.FC = () => { - const colors = useThemeSlice('colors'); - - return ( -
-

My primary color is: {colors.primary}

-

My secondary color is: {colors.secondary}

-
- ); -}; -``` - -### useThemeValue - -```jsx -const MyComponent: React.FC = () => { - const primaryColor = useThemeValue('colors', 'primary'); - - return ( -
-

My primary color is: {primaryColor}

-
- ); -}; -``` - -## useStyles - -If you don't need the theme, but to generate a style based on the theme; The hooks `useStyles` and `useStyle` are made for this reason: - -```jsx -const MyComponent: React.FC = () => { - const { agreeStyle, disagreeStyle, textStyle } = useStyles({ - agreeStyle: { componentName: 'Button', variant: 'success' }, - disagreeStyle: { componentName: 'Button' }, - textStyle: { fontSize: '2xl', color: 'white' }, - }); - - return ( -
-

Nothing is better than a fresh beer in summer 🍺

- - -
- ); -}; -``` - -## useStyle - -Use it if you need to generate just one style: - -```jsx -const AgreeButton: React.FC = ({ children }) => { - const buttonStyle = useStyles({ - componentName: 'Button', - variant: 'success', - }); - - return ; -}; -``` - -> Just like useTheme, **useStyles** and **useStyle** are the equivalent of the [core API's](https://morfeo.dev/docs/Packages/core) `morfeo.resolve(style)` -> but they force a re-render when the theme changes. - -## useProps - -Use it to get the default props of a components, a common use is to merge the default props with the current props: - -```jsx -function MyButton(props) { - const defaultProps = useProps('Button', 'primary'); +## Documentation - return +
+ + Try to click on the "Toggle theme" button and see the current theme + changing +

); } ``` -If instead we use directly the `morfeo` instance from the core-api, the component will not re-render when the theme changes: +If instead we use directly the `morfeo` instance from the core-api without using `useSyncMorfeo`, the component will not re-render when the theme changes: ```tsx live function App() { + // useSyncMorfeo(); + const onClick = () => { morfeo.setCurrentTheme( morfeo.getCurrentTheme() === 'light' ? 'dark' : 'light', @@ -214,6 +219,10 @@ function App() { The current theme is: {morfeo.getCurrentTheme()}
+
+ + Try to click on the "Toggle theme" button and see that nothing changes +

); } diff --git a/docs/docs/Packages/react.mdx b/docs/docs/Packages/react.mdx index cbf6df1c..23b23632 100644 --- a/docs/docs/Packages/react.mdx +++ b/docs/docs/Packages/react.mdx @@ -48,17 +48,18 @@ function useClassNames( ### Example -```tsx +```tsx live function Button() { const classes = useClassNames({ container: { px: 'm', }, button: { + corner: 'm', p: { lg: 'm', sm: 's' }, bg: 'primary', '&:hover': { - opacity: 'light', + bg: 'primary.lightest', }, }, }); @@ -79,13 +80,14 @@ function useClassName(style: Style): string; ### Example -```tsx +```tsx live function Button() { const className = useClassName({ + corner: 'm', p: { lg: 'm', sm: 's' }, bg: 'primary', '&:hover': { - opacity: 'light', + bg: 'primary.lightest', }, }); diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 7b0c7f50..8e9534dc 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -1,5 +1,5 @@ export * from './useTheme'; export * from './useProps'; export * from './useStyles'; -export * from './useMorfeo'; +export * from './useSyncMorfeo'; export * from './useCurrentTheme'; diff --git a/packages/hooks/src/useCurrentTheme.ts b/packages/hooks/src/useCurrentTheme.ts index 649cfd1d..ca0baceb 100644 --- a/packages/hooks/src/useCurrentTheme.ts +++ b/packages/hooks/src/useCurrentTheme.ts @@ -1,9 +1,9 @@ -import { ThemeName } from '@morfeo/core'; -import { useMorfeo } from './useMorfeo'; +import { morfeo, ThemeName } from '@morfeo/core'; +import { useSyncMorfeo } from './useSyncMorfeo'; type UseCurrentThemeReturnValue = [ThemeName, (name: ThemeName) => void]; export function useCurrentTheme(): UseCurrentThemeReturnValue { - const { getCurrentTheme, setCurrentTheme } = useMorfeo(); - return [getCurrentTheme(), setCurrentTheme]; + useSyncMorfeo(); + return [morfeo.getCurrentTheme(), morfeo.setCurrentTheme]; } diff --git a/packages/hooks/src/useProps.ts b/packages/hooks/src/useProps.ts index 97b2466c..62380dde 100644 --- a/packages/hooks/src/useProps.ts +++ b/packages/hooks/src/useProps.ts @@ -1,5 +1,5 @@ import { Component, Theme, theme, Variant } from '@morfeo/core'; -import { useMorfeo } from './useMorfeo'; +import { useSyncMorfeo } from './useSyncMorfeo'; /** * useProps @@ -12,7 +12,7 @@ export function useProps( componentName: C, variant?: Variant, ) { - useMorfeo(); + useSyncMorfeo(); const { props, variants = {} } = theme.getValue('components', componentName) || {}; const { props: variantProps } = (variants as any)[variant] || {}; diff --git a/packages/hooks/src/useStyles.ts b/packages/hooks/src/useStyles.ts index 2770a0c9..976de924 100644 --- a/packages/hooks/src/useStyles.ts +++ b/packages/hooks/src/useStyles.ts @@ -1,5 +1,5 @@ import { Style, ResolvedStyle, morfeo } from '@morfeo/core'; -import { useMorfeo } from './useMorfeo'; +import { useSyncMorfeo } from './useSyncMorfeo'; function parseStyles(styles: Record) { const styleKeys = Object.keys(styles); @@ -17,7 +17,7 @@ function parseStyles(styles: Record) { * it returns the a record of styles that can be used as inline-style in your components. */ export function useStyles(styles: Record) { - useMorfeo(); + useSyncMorfeo(); return parseStyles(styles); } diff --git a/packages/hooks/src/useMorfeo.ts b/packages/hooks/src/useSyncMorfeo.ts similarity index 58% rename from packages/hooks/src/useMorfeo.ts rename to packages/hooks/src/useSyncMorfeo.ts index e054b736..e6af77cc 100644 --- a/packages/hooks/src/useMorfeo.ts +++ b/packages/hooks/src/useSyncMorfeo.ts @@ -7,11 +7,8 @@ function subscribe(...callback: Parameters) { } /** - * It returns the `morfeo instance` and subscribes the component/hook - * where is used to theme changes + * It subscribes the component/hook where is used to theme changes */ -export function useMorfeo() { - useSyncExternalStore(subscribe, morfeo.getTheme); - - return morfeo; +export function useSyncMorfeo() { + return useSyncExternalStore(subscribe, morfeo.getTheme); } diff --git a/packages/hooks/src/useTheme.ts b/packages/hooks/src/useTheme.ts index 08550b10..191c4e3d 100644 --- a/packages/hooks/src/useTheme.ts +++ b/packages/hooks/src/useTheme.ts @@ -1,5 +1,5 @@ -import { ThemeKey, Theme } from '@morfeo/core'; -import { useMorfeo } from './useMorfeo'; +import { morfeo, ThemeKey, Theme } from '@morfeo/core'; +import { useSyncMorfeo } from './useSyncMorfeo'; /** * Same as `morfeo.getTheme()` but it will cause a re-render @@ -8,8 +8,8 @@ import { useMorfeo } from './useMorfeo'; * @returns the theme object */ export function useTheme() { - const { getTheme } = useMorfeo(); - return getTheme(); + useSyncMorfeo(); + return morfeo.getTheme(); } /** diff --git a/packages/hooks/tests/useMorfeo.test.ts b/packages/hooks/tests/useMorfeo.test.ts deleted file mode 100644 index 542a14bb..00000000 --- a/packages/hooks/tests/useMorfeo.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { renderHook, act } from '@testing-library/react'; -import { morfeo, theme, Theme, ThemeName } from '@morfeo/core'; -import { useMorfeo } from '../src'; - -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; - -beforeEach(() => { - 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.getTheme()).toEqual(LIGHT_THEME); - }); - - describe('when the theme changes', () => { - it('should return the dark theme', () => { - const { result } = renderHook(() => useMorfeo()); - - act(() => { - result.current.setCurrentTheme(DARK_THEME_KEY); - }); - - expect(result.current.getTheme()).toEqual(DARK_THEME); - }); - }); -}); diff --git a/packages/hooks/tests/useSyncMorfeo.test.ts b/packages/hooks/tests/useSyncMorfeo.test.ts new file mode 100644 index 00000000..b2bd7446 --- /dev/null +++ b/packages/hooks/tests/useSyncMorfeo.test.ts @@ -0,0 +1,48 @@ +import { renderHook, act } from '@testing-library/react'; +import { morfeo, Theme, ThemeName } from '@morfeo/core'; +import { useSyncMorfeo } from '../src'; + +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; + +function useHookForTest() { + useSyncMorfeo(); + + return morfeo.getCurrentTheme(); +} + +describe('useSyncMorfeo', () => { + beforeEach(() => { + morfeo.setTheme(LIGHT_THEME_KEY, LIGHT_THEME); + morfeo.setTheme(DARK_THEME_KEY, DARK_THEME); + morfeo.setCurrentTheme(LIGHT_THEME_KEY); + }); + + describe('when the theme changes', () => { + it('should trigger a re-render and return the dark theme', () => { + const { result } = renderHook(() => useHookForTest()); + const oldThemeName = result.current; + + act(() => { + morfeo.setCurrentTheme(DARK_THEME_KEY); + }); + + expect(oldThemeName).toBe(LIGHT_THEME_KEY); + expect(result.current).toBe(DARK_THEME_KEY); + }); + }); +}); diff --git a/packages/react/src/useClassName.ts b/packages/react/src/useClassName.ts index eaae59f3..3408be77 100644 --- a/packages/react/src/useClassName.ts +++ b/packages/react/src/useClassName.ts @@ -1,11 +1,11 @@ -import { useState, useEffect } from 'react'; +import { useState, useInsertionEffect } from 'react'; import stringify from 'fast-safe-stringify'; import { Style, getStyles } from '@morfeo/web'; export function useClassNames(styles: Record) { const [{ update, classes }] = useState(() => getStyles(styles)); - useEffect(() => { + useInsertionEffect(() => { update(styles); }, [stringify(styles)]); From 5cd5cebfd579add290f12cc3ff65ee1ce7b6165e Mon Sep 17 00:00:00 2001 From: mauroerta Date: Mon, 30 May 2022 16:29:07 +0200 Subject: [PATCH 4/4] fix: added server side snapshot callback --- packages/hooks/src/useSyncMorfeo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hooks/src/useSyncMorfeo.ts b/packages/hooks/src/useSyncMorfeo.ts index e6af77cc..d97ac908 100644 --- a/packages/hooks/src/useSyncMorfeo.ts +++ b/packages/hooks/src/useSyncMorfeo.ts @@ -10,5 +10,5 @@ function subscribe(...callback: Parameters) { * It subscribes the component/hook where is used to theme changes */ export function useSyncMorfeo() { - return useSyncExternalStore(subscribe, morfeo.getTheme); + return useSyncExternalStore(subscribe, morfeo.getTheme, morfeo.getTheme); }