0

It looks cumbersome when I have to wrap tons of action dispatchers into useCallback() to prevent unnecessary rerendering of subcomponents:

const [{ /* ... */ }, dispatch] = useReducer(reducer, initialState);

const toggleImageActive = useCallback((id: string) => {
  dispatch({ type: 'toggle image active', id });
}, []);

const deleteImage = useCallback((id: string) => {
  dispatch({ type: 'delete image', id });
}, []);

const addImages = useCallback((images: ImageFileData[]) => {
  dispatch({ type: 'add images', images });
}, []);

const setBoard = useCallback((board: number) => {
  dispatch({ type: 'set board', board });
}, []);

const setTags = useCallback((tags: Tag[]) => {
  dispatch({ type: 'set tags', tags });
}, []);

// etc...

Is there a way to do it in a more elegant way?

  • why not just pass `dispatch` to subcomponent – Giorgi Moniava Aug 26 '22 at 18:49
  • @Giorgi Moniava, that would lead to high coupling of components and I wouldn't be able to reuse subcomponents anywhere else. – Alexander Korostin Aug 28 '22 at 09:12
  • I think you should read [this](https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/#memoize-everything) – Arkellys Aug 28 '22 at 14:19
  • @Arkellys, thank you for your advice, but I think that's not the case. I just have a lot of components on this page and without memoizing the callbacks almost all of them would rerender on each change event from custom input widgets which cannot be transformed to uncontrolled ones. – Alexander Korostin Aug 28 '22 at 15:15

2 Answers2

0

I don't know why your subcomponents would be re-rendering unless you are passing these constant functions into the props of the subcomponents. I would put these const statements outside of your component, perhaps into a separate file using the export keyword. Then you can import them into the parent component and its subcomponents.

A problem with this approach is that you use dispatch, which is defined within the parent component. You could include dispatch as a parameter that you pass into these functions, although that clutters the functions a bit. If I were you, I'd try importing the store itself and call dispatch on it directly instead of using the hook, as is described here.

0

The best solution I came up with is to create a function similar to Redux connect():

export function connect<
  R extends Reducer<any, any>,
  SP extends {} = {},
  DP extends {} = {}
>(
  reducer: R,
  initialState: ReducerState<R>,
  mapStateToProps?: (state: ReducerState<R>) => SP,
  mapDispatchToProps?: (dispatch: Dispatch<ReducerAction<R>>) => DP,
) {
  return <CP extends SP & DP>(WrappedComponent: ComponentType<CP>) => {
    const ConnectWrapper: FC<Omit<CP, keyof SP | keyof DP>> = (props) => {
      const [state, dispatch] = useReducer(reducer, initialState);
      const dispatchProps = useMemo(() => {
        if (!mapDispatchToProps) return {} as DP;
        return mapDispatchToProps(dispatch);
      }, [dispatch]);
      const allProps = {
        ...props,
        ...(mapStateToProps ? mapStateToProps(state) : {} as SP),
        ...dispatchProps,
      } as CP;
      return <WrappedComponent {...allProps} />;
    };
    ConnectWrapper.displayName = 'ConnectWrapper';
    return ConnectWrapper;
  };
}

And then using it with a component:

interface TestProps {
  text: string
  enabled: boolean;
}

const Test: FC<TestProps> = ({ text }) => <>{text}</>;

export default connect(
  reducer,
  initialState,
  state => ({
    enabled: state.enabled,
  }),
  dispatch => ({
    enable: () => dispatch({ type: 'enable' }),
  }),
)(Test);

But when I started using it, it turned out that it was far from ideal:

  1. You have to declare the function and state types in the *Props interface.
  2. Then implement functions in the mapDispatchToProps() callback duplicating type definitions.
  3. Type definitions from the *Props interface are not visible in map*ToProps() functions. So it's possible to make a mistake there. It won't compile though, but it seems rather inconvenient that the definitions do not appear in IntelliSense.
  4. Type definitions and function realizations are located in different places with the component's code in between.
  5. It's either type-unsafe or cumbersome to map only portion of the state to component props depending on which approach you choose: copy all the props one by one or use something like pick() from lodash.