Recompose: Is it possible to wrap event handlers in a parent object?

Created on 18 Dec 2017  路  12Comments  路  Source: acdlite/recompose

_I would normally ask questions like this on stackoverflow but I think this is a little too specific. 馃槃_

I don't really like passing prop names all over the place especially when they are created inside the component itself via recompose.

Thinking about this is dawned on my that I can wrap the state from withStateHandlers and props from withProps in a parent object and simply pass that object to the primary component. This works pretty well. What I would also love is to create a myHandlers object that wraps the handlers.

Is there a way to make that happen?

This code is simplified example but shows the approach.
Ideally a myHandlers prop would wrap the menu handlers negating the need to pull in each individually.

Possible?
Thanks for any ideas/help!

const OverflowButton = ({
  myProps, myState, hideMenu, showMenu,
}) => (
  <View>
    <IconButton onPress={showMenu} name={myProps.dotsIconStyle} size={24} />
    <ModalMenu visible={myState.isMenuVisible} onRequestClose={hideMenu} />
  </View>
);

const myState = withStateHandlers(
  {
    myState: {
      isMenuVisible: false,
    },
  },
  {
    showMenu: () => () => ({
      myState: { isMenuVisible: true },
    }),
    hideMenu: () => () => ({
      myState: { isMenuVisible: false },
    }),
  },
);

const myProps = withProps(() => ({
  myProps: {
    dotsIconStyle: Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical',
  },
}));

export default compose(myState, myProps)(OverflowButton);

Most helpful comment

Hi @istarkov
Posting my finalized version in case anyone is interested.
This version removes the duplicate startingProps siblings.

The benefit here is having one place to look when determining where props come from in addition to being able to pass them all in one batch to the stateless component.

Also, it's easy to identify props passed in from the outside as the won't start with my.... _color_ in this example.

import { compose, withProps, mapProps } from 'recompose';

const PropsToNamespace = (namespaceName, ...hocs) =>
  compose(
    // Store existing props for later use.
    withProps(props => ({ startingProps: props })),

    // Compose passed in HOCs.
    ...hocs,

    // Map props from HOCs into the desired namespace.
    mapProps(({ startingProps, ...existingPropsAndNewProps}) => {

      // Clear out pre existing props from newProps.
      const newProps = Object.assign({}, existingPropsAndNewProps);
      if (startingProps) {
        Object.keys(startingProps).forEach((key) => {
          delete newProps[key];
        });
      }

      return {
        ...startingProps,
        [namespaceName]: newProps,
      };
    }),
  );

export default PropsToNamespace;

Usage

import { PropsToNamespace } from 'path/to/your/lib';

const OverflowButton = ({ myProps, myState, color }) => (
  <View>
    <IconButton onPress={myState.showMenu} name={myProps.dotsIconStyle} size={24} color={color} />
    <ModalMenu visible={myState.isMenuVisible} onRequestClose={myState.hideMenu} />
  </View>
);

const myState = withStateHandlers(
  {
    myState: {
      isMenuVisible: false,
    },
  },
  {
    showMenu: () => () => ({
      myState: { isMenuVisible: true },
    }),
    hideMenu: () => () => ({
      myState: { isMenuVisible: false },
    }),
  },
);

const myProps = withProps(() => ({
  dotsIconStyle: Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical',
}));

export default compose(
  PropsToNamespace('myState', myState), 
  PropsToNamespace('myProps', myProps)
)(OverflowButton);

All 12 comments

reread few times, havent got the question :-)
I see no myHandlers in your example, can you provide what you want at the end.

Seems like I got, I used something like this in the past
namespace hoc:

export const namespace = (ns, ...hocs) =>
  compose(
    withProps(props => ({ $parentProps: props })), // TODO $parentProps as symbol
    ...hocs,
    mapProps(({$parentProps, ...props}) => ({ ...ns(props), ...$parentProps })),
  );

usage

  namespace(
    props => ({ ANY_PROP_NAME_YOU_WANT: props }),
    ... ANY AMOUNT OF YOUR OR RECOMPOSE HOCS ...,
  )

So any props generated by HOCs inside namespace will be placed inside property ANY_PROP_NAME_YOU_WANT

It's poorly written, it does not support nesting, but for real simple needs it works well

PS: at usage above it will also place props from HOCs above, feel free to use pick in first mapper function or somehow change the namespace,
it depends on what you want.
Also you can override withStateHandlers HOC etc.

```javascript
const myStateHandlers = (initialState, handlers) => {
return compose(
withStateHandlers(initialState, handlers),
mapProps(props => {
// HERE YOU KNOW THE KEYS IN initialState and in a handlers so can copy what you need into a property
})
)

}

Hi @istarkov. Thanks so much for the help!

I ended up going with this...

const PropsToNamespace = (namespaceName, ...hocs) =>
  compose(
    withProps(props => ({ startingProps: props })),
    ...hocs,
    mapProps(({ startingProps, ...propsFromHOCs }) => ({
      [namespaceName]: propsFromHOCs,
      ...startingProps,
    })),
  );

Which simplifies the usage to this...

PropsToNamespace(
  'ANY_NAMESPACE_NAME_YOU_WANT',
  ... ANY AMOUNT OF YOUR OR RECOMPOSE HOCS ...,
)

One weird issue I've run into with this...
If I try and console.log a namespace it crashes my app.

<Button title="hi!!" onPress={() => console.log('yo:', myProps)} />

CRASH 馃挘

But, if I log a property hanging off the namespace it works as expected.

<Button title="hi!!" onPress={() => console.log('yo:', myProps.dotsIconStyle)} />

Have you experience anything like this before?

My first program I wrote in 1991 I experienced strange bugs thousand times ;-) Is it crash in all browsers?

Ha! 1991... Cool!
Not running in any browsers. It's a react native app.

I was just wondering if you have experienced any issues when logging a namespaced prop after using the code you provided.

If you haven't that's great.

Thanks again for the help!

Hi @istarkov
Posting my finalized version in case anyone is interested.
This version removes the duplicate startingProps siblings.

The benefit here is having one place to look when determining where props come from in addition to being able to pass them all in one batch to the stateless component.

Also, it's easy to identify props passed in from the outside as the won't start with my.... _color_ in this example.

import { compose, withProps, mapProps } from 'recompose';

const PropsToNamespace = (namespaceName, ...hocs) =>
  compose(
    // Store existing props for later use.
    withProps(props => ({ startingProps: props })),

    // Compose passed in HOCs.
    ...hocs,

    // Map props from HOCs into the desired namespace.
    mapProps(({ startingProps, ...existingPropsAndNewProps}) => {

      // Clear out pre existing props from newProps.
      const newProps = Object.assign({}, existingPropsAndNewProps);
      if (startingProps) {
        Object.keys(startingProps).forEach((key) => {
          delete newProps[key];
        });
      }

      return {
        ...startingProps,
        [namespaceName]: newProps,
      };
    }),
  );

export default PropsToNamespace;

Usage

import { PropsToNamespace } from 'path/to/your/lib';

const OverflowButton = ({ myProps, myState, color }) => (
  <View>
    <IconButton onPress={myState.showMenu} name={myProps.dotsIconStyle} size={24} color={color} />
    <ModalMenu visible={myState.isMenuVisible} onRequestClose={myState.hideMenu} />
  </View>
);

const myState = withStateHandlers(
  {
    myState: {
      isMenuVisible: false,
    },
  },
  {
    showMenu: () => () => ({
      myState: { isMenuVisible: true },
    }),
    hideMenu: () => () => ({
      myState: { isMenuVisible: false },
    }),
  },
);

const myProps = withProps(() => ({
  dotsIconStyle: Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical',
}));

export default compose(
  PropsToNamespace('myState', myState), 
  PropsToNamespace('myProps', myProps)
)(OverflowButton);

@GollyJer It looks like you didn't use PropsToNamespace in your usage example.

@wuct Good catch. 馃槂
The example is updated.

I'm going to close this issue. Please feel free to reopen it if you have further thought.

Was this page helpful?
0 / 5 - 0 ratings