akveo / react-native-ui-kitten

:boom: React Native UI Library based on Eva Design System :new_moon_with_face::sparkles:Dark Mode
https://akveo.github.io/react-native-ui-kitten/
MIT License
10.36k stars 958 forks source link

useStyles hook #1436

Open sschottler opened 3 years ago

sschottler commented 3 years ago

🚀 Feature Proposal

An optional useStyles('CustomComponent', props) hook alternative to styled HOC/decorator for custom components.

Motivation

A hook could lead to cleaner api/less typing/less wrapper components in component tree when you inspect...

Example

I have a somewhat working implementation below. I needed StyleConsumerService and MappingContext to get it working, but they aren't exported at package level so the direct file imports feel a little brittle since a new version update could change internal structure.

import { useContext, useState, useMemo } from 'react';
import { useTheme } from '@ui-kitten/components';
import { StyleConsumerService } from '@ui-kitten/components/theme/style/styleConsumer.service';
import { MappingContext } from '@ui-kitten/components/theme/mapping/mappingContext';

export function useStyles<P = any>(componentName: string, sourceProps: P): any {
  const [interaction, dispatch] = useState([]);
  const theme = useTheme();
  const mappingContext = useContext(MappingContext);
  const service = useMemo(() => new StyleConsumerService(componentName, mappingContext), [
    componentName,
    mappingContext,
  ]);
  const defaultProps = useMemo(() => service.createDefaultProps(), [service]);
  const props = { ...defaultProps, ...sourceProps };
  const style = service.createStyleProp(props, mappingContext, theme, interaction);

  // return eva object:
  return {
    theme,
    style,
    dispatch,
  };
}

Usage

export const CustomComponent = props => {
    // needs the props to determine variants:
    const eva = useStyles('CustomComponent', props);
    // convert eva mapping token values to style object:
    const styles = getComponentStyles(eva.style); 

    // eva.dispatch([Interaction.HOVER]); possible to change interaction state and trigger rerender styles lookup 

    return <View style={styles.container}>...</View>;
}

// vs styled hoc:

const CustomComponent = props => {
   const evaStyles = props.eva.style;
   // convert eva mapping token values to style object:
   const styles = getComponentStyles(evaStyles); 
   return <View style={styles.container}>...</View>;
}
const StyledCustomComponent = styled('CustomComponent')(CustomComponent);
export { StyledCustomComponent as CustomComponent };
RWOverdijk commented 3 years ago

There's already an open issue for this here.

sschottler commented 3 years ago

There's already an open issue for this here.

Ah yeah, that overlaps and has more fleshed out examples of the hook. If they keep that issue open for the hook, then this one should be closed. I'll leave this issue open for now in case they close the other issue because the original question has a solution that doesn't require the hook (styled decorator as HOC as you posted - I'm also using this approach on my project)

RWOverdijk commented 3 years ago

Pasting the comment I wrote for the related issue:

is this something that could be added to ui kitten? I think it would be nice to have. I changed it a bit to make the prop types remain:

import { StyledComponentProps } from '@ui-kitten/components/theme/style/styled';
import { useCallback, useContext, useMemo, useState } from 'react';
import { MappingContext } from '@ui-kitten/components/theme/mapping/mappingContext';
import { ThemeContext } from '@ui-kitten/components/theme/theme/themeContext';
import { StyleConsumerService } from '@ui-kitten/components/theme/style/styleConsumer.service';

export function useStyled<T>(name: string, sourceProps: T): T & StyledComponentProps {
  const [interaction, setInteraction] = useState([]);
  const mappingStyle = useContext(MappingContext);
  const theme = useContext(ThemeContext);
  const service = useMemo(() => new StyleConsumerService(name, mappingStyle), [name, mappingStyle]);
  const defaultProps = useMemo(() => service.createDefaultProps(), [service]);
  const computedProps = useMemo(() => ({ ...defaultProps, ...sourceProps }), [defaultProps, sourceProps]);
  const dispatch = useCallback((newInteraction: any) => setInteraction(newInteraction), [setInteraction]);
  const style = useMemo(() => service?.createStyleProp(computedProps, mappingStyle, theme, interaction), [
    service,
    computedProps,
    mappingStyle,
    theme,
    interaction
  ]);

  return useMemo(() => ({
    ...computedProps,
    eva: { theme, style, dispatch }
  }), [computedProps, theme, style]);
}

Usage:

import React from 'react';
import { View } from 'react-native';
import { FooterStyle } from './Footer.style';
import { ExternalLink } from '../ExternalLink';

import { useStyled } from '../../lib/hooks/useStyled';

interface FooterProps { }

export function Footer(props: FooterProps) {
  const { eva: { style = null } = {} } = useStyled('Footer', props);

  return (
    <View style={[FooterStyle.container, style]}>
      <ExternalLink href="https://google.com/">Go to link!</ExternalLink>
    </View>
  )
}