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:
- You have to declare the function and state types in the *Props interface.
- Then implement functions in the
mapDispatchToProps()
callback duplicating type definitions.
- 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.
- Type definitions and function realizations are located in different places with the component's code in between.
- 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
.