How to update react HOC component with redux state?

Below is the HOC and it is connected to redux store too. The WrappedComponent function is not fetching the redux state on change of storedata. What could be wrong here?

export function withCreateHOC<ChildProps>(
  ChildComponent: ComponentType,
  options: WithCreateButtonHOCOptions = {
    title: 'Create',
  },
) {
  function WrappedComponent(props: any) {
    const { createComponent, title } = options;
    const [isOpen, setisOpen] = useState(false);

    function onCreateClick() {
      setisOpen(!isOpen);
      Util.prevDefault(() => setisOpen(isOpen));
    }

    return (
      <div>
        <ChildComponent {...props} />
        <div>
          <Component.Button
            key={'add'}
            big={true}
            round={true}
            primary={true}
            onClick={Util.prevDefault(onCreateClick)}
            className={'float-right'}
            tooltip={title}
          >
            <Component.Icon material={'add'} />
          </Component.Button>
        </div>
        <OpenDrawerWithClose
          open={isOpen}
          title={title}
          setisOpen={setisOpen}
          createComponent={createComponent}
        />
      </div>
    );
  }

  function mapStateToProps(state: any) {
    console.log('HOC mapStateToProps isOpen', state.isOpen);
    return {
      isOpen: state.isOpen,
    };
  }
  // Redux connected;
  return connect(mapStateToProps, {})(WrappedComponent);
}

Expecting isOpen to be used from ReduxStore and update the same with WrappedComponent here. By any chance this should be changed to class component?

The above HOC is used as:

export const Page = withCreateHOC(
  PageItems,
  {
    createComponent: <SomeOtherComponent />,
    title: 'Create',
  },
);

36 thoughts on “How to update react HOC component with redux state?”

  1. Overview

    You don’t want isOpen to be a local state in WrappedComponent. The whole point of this HOC is to access isOpen from your redux store. Note that nowhere in this code are you changing the value of your redux state. You want to ditch the local state, access isOpen from redux, and dispatch an action to change isOpen in redux.

    Additionally we’ve got to replace some of those anys with actual types!

    It seems a little suspect to me that you are passing a resolved JSX element rather than a callable component as createComponent (<SomeOtherComponent /> vs SomeOtherComponent), but whether that is correct or a mistake depends on what’s in your OpenDrawerWithClose component. I’m going to assume it’s correct as written here.

    There’s nothing technically wrong with using connect, but it feels kinda weird to use an HOC inside of an HOC so I am going to use the hooks useSelector and useDispatch instead.

    Step By Step

    We want to create a function that takes a component ComponentType<ChildProps> and some options WithCreateButtonHOCOptions. You are providing a default value for options.title so we can make it optional. Is options.createComponent optional or required?

    interface WithCreateButtonHOCOptions {
        title: string;
        createComponent: React.ReactNode;
    }
    
    function withCreateHOC<ChildProps>(
      ChildComponent: ComponentType<ChildProps>,
      options: Partial<WithCreateButtonHOCOptions>
    ) {
    

    We return a function that takes the same props, but without isOpen or toggleOpen, if those were properties of ChildProps.

    return function (props: Omit<ChildProps, 'isOpen' | 'toggleOpen'>) {
    

    We need to set defaults for the options in the destructuring step in order to set only one property.

    const { createComponent, title = 'Create' } = options;
    

    We access isOpen from the redux state.

    const isOpen = useSelector((state: { isOpen: boolean }) => state.isOpen);
    

    We create a callback that dispatches an action to redux — you will need to handle this in your reducer. I am dispatching a raw action object {type: 'TOGGLE_OPEN'}, but you could make an action creator function for this.

    const dispatch = useDispatch();
    const toggleOpen = () => {
        dispatch({type: 'TOGGLE_OPEN'});
    }
    

    We will pass these two values isOpen and toggleOpen as props to ChildComponent just in case it want to use them. But more importantly, we can use them as click handlers on your button and drawer components. (Note: it looks like drawer wants a prop setIsOpen that takes a boolean, so you may need to tweak this a bit. If the drawer is only shown when isOpen is true then just toggling should be fine).

    Code

    function withCreateHOC<ChildProps>(
        ChildComponent: ComponentType<ChildProps>,
        options: Partial<WithCreateButtonHOCOptions>
    ) {
        return function (props: Omit<ChildProps, 'isOpen' | 'toggleOpen'>) {
            const { createComponent, title = 'Create' } = options;
    
            const isOpen = useSelector((state: { isOpen: boolean }) => state.isOpen);
    
            const dispatch = useDispatch();
            const toggleOpen = () => {
                dispatch({ type: 'TOGGLE_OPEN' });
            }
    
            return (
                <div>
                    <ChildComponent
                        {...props as ChildProps}
                        toggleOpen={toggleOpen}
                        isOpen={isOpen}
                    />
                    <div>
                        <Component.Button
                            key={'add'}
                            big={true}
                            round={true}
                            primary={true}
                            onClick={toggleOpen}
                            className={'float-right'}
                            tooltip={title}
                        >
                            <Component.Icon material={'add'} />
                        </Component.Button>
                    </div>
                    <OpenDrawerWithClose
                        open={isOpen}
                        title={title}
                        setisOpen={toggleOpen}
                        createComponent={createComponent}
                    />
                </div>
            );
        }
    }
    

    This version is slightly better because it does not have the as ChildProps assertion. I don’t want to get too sidetracked into the "why" but basically we need to insist that if ChildProps takes an isOpen or toggleOpen prop, that those props must have the same types as the ones that we are providing.

    interface AddedProps {
        isOpen: boolean;
        toggleOpen: () => void;
    }
    
    function withCreateHOC<ChildProps>(
        ChildComponent: ComponentType<Omit<ChildProps, keyof AddedProps> & AddedProps>,
        options: Partial<WithCreateButtonHOCOptions>
    ) {
        return function (props: Omit<ChildProps, keyof AddedProps>) {
            const { createComponent, title = 'Create' } = options;
    
            const isOpen = useSelector((state: { isOpen: boolean }) => state.isOpen);
    
            const dispatch = useDispatch();
            const toggleOpen = () => {
                dispatch({ type: 'TOGGLE_OPEN' });
            }
    
            return (
                <div>
                    <ChildComponent
                        {...props}
                        toggleOpen={toggleOpen}
                        isOpen={isOpen}
                    />
                    <div>
                        <Component.Button
                            key={'add'}
                            big={true}
                            round={true}
                            primary={true}
                            onClick={toggleOpen}
                            className={'float-right'}
                            tooltip={title}
                        >
                            <Component.Icon material={'add'} />
                        </Component.Button>
                    </div>
                    <OpenDrawerWithClose
                        open={isOpen}
                        title={title}
                        setisOpen={toggleOpen}
                        createComponent={createComponent}
                    />
                </div>
            );
        }
    }
    

    Playground Link

    Reply

Leave a Comment