Start using react redux hooks (#845)

* Upgrade react-redux

* Upgrade react-redux types

* Start adopting react-redux hooks
This commit is contained in:
Jordan Eldredge 2019-08-09 10:10:20 -07:00
parent 61c0afb165
commit d8b33e4795
16 changed files with 175 additions and 268 deletions

View file

@ -64,7 +64,9 @@ export function toggleEq(): Thunk {
}
export function toggleEqAuto(): Thunk {
return (dispatch, getState) => {
dispatch({ type: SET_EQ_AUTO, value: !getState().equalizer.auto });
return dispatch => {
// We don't actually support this feature yet so don't let the user ever turn it on.
// dispatch({ type: SET_EQ_AUTO, value: !getState().equalizer.auto });
dispatch({ type: SET_EQ_AUTO, value: false });
};
}

View file

@ -1,34 +1,15 @@
import React from "react";
import { connect } from "react-redux";
import classnames from "classnames";
import { SET_EQ_AUTO } from "../../actionTypes";
import { Dispatch, AppState } from "../../types";
import * as Actions from "../../actionCreators";
import { useTypedSelector, useActionCreator } from "../../hooks";
interface StateProps {
auto: boolean;
}
const EqAuto = React.memo(() => {
const selected = useTypedSelector(state => state.equalizer.auto);
const toggleAuto = useActionCreator(Actions.toggleEqAuto);
return (
<div id="auto" className={classnames({ selected })} onClick={toggleAuto} />
);
});
interface DispatchProps {
toggleAuto(): void;
}
const EqAuto = (props: StateProps & DispatchProps) => {
const className = classnames({ selected: props.auto });
return <div id="auto" className={className} onClick={props.toggleAuto} />;
};
const mapStateToProps = (state: AppState): StateProps => {
return { auto: state.equalizer.auto };
};
const mapDispatchToProps = () => (dispatch: Dispatch): DispatchProps => {
// We don't support auto.
return {
toggleAuto: () => dispatch({ type: SET_EQ_AUTO, value: false }),
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(EqAuto);
export default EqAuto;

View file

@ -1,37 +1,21 @@
import React from "react";
import { connect } from "react-redux";
import { previous, play, pause, stop, next } from "../../actionCreators";
import { Dispatch } from "../../types";
import * as Actions from "../../actionCreators";
import { useActionCreator } from "../../hooks";
interface DispatchProps {
previous(): void;
play(): void;
pause(): void;
stop(): void;
next(): void;
}
const ActionButtons = React.memo(() => {
const previous = useActionCreator(Actions.previous);
const play = useActionCreator(Actions.play);
const pause = useActionCreator(Actions.pause);
const next = useActionCreator(Actions.next);
return (
<div className="actions">
<div id="previous" onClick={previous} title="Previous Track" />
<div id="play" onClick={play} title="Play" />
<div id="pause" onClick={pause} title="Pause" />
<div id="stop" onClick={stop} title="Stop" />
<div id="next" onClick={next} title="Next Track" />
</div>
);
});
const ActionButtons = (props: DispatchProps) => (
<div className="actions">
<div id="previous" onClick={props.previous} title="Previous Track" />
<div id="play" onClick={props.play} title="Play" />
<div id="pause" onClick={props.pause} title="Pause" />
<div id="stop" onClick={props.stop} title="Stop" />
<div id="next" onClick={props.next} title="Next Track" />
</div>
);
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
return {
previous: () => dispatch(previous()),
play: () => dispatch(play()),
pause: () => dispatch(pause()),
stop: () => dispatch(stop()),
next: () => dispatch(next()),
};
};
export default connect(
null,
mapDispatchToProps
)(ActionButtons);
export default ActionButtons;

View file

@ -1,23 +1,12 @@
import React from "react";
import { connect } from "react-redux";
import ClickedDiv from "../ClickedDiv";
import { useActionCreator } from "../../hooks";
import { close } from "../../actionCreators";
import { Dispatch } from "../../types";
import * as Actions from "../../actionCreators";
interface DispatchProps {
onClick: () => void;
}
const Close = React.memo(() => {
const close = useActionCreator(Actions.close);
return <ClickedDiv id="close" onClick={close} title="Close" />;
});
const Close = ({ onClick }: DispatchProps) => (
<ClickedDiv id="close" onClick={onClick} title="Close" />
);
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
return { onClick: () => dispatch(close()) };
};
export default connect(
null,
mapDispatchToProps
)(Close);
export default Close;

View file

@ -1,53 +1,45 @@
import React from "react";
import { connect } from "react-redux";
import classnames from "classnames";
import { SET_FOCUS, UNSET_FOCUS } from "../../actionTypes";
import { toggleDoubleSizeMode } from "../../actionCreators";
import { AppState, Dispatch } from "../../types";
import * as Actions from "../../actionCreators";
import { Action, Dispatch, Thunk } from "../../types";
import OptionsContextMenu from "../OptionsContextMenu";
import ContextMenuTarget from "../ContextMenuTarget";
import { useActionCreator, useTypedSelector } from "../../hooks";
import * as Selectors from "../../selectors";
interface StateProps {
doubled: boolean;
function setFocusDouble(): Action {
return Actions.setFocus("double");
}
interface DispatchProps {
handleMouseDown(): void;
handleMouseUp(): void;
function mouseUp(): Thunk {
return dispatch => {
dispatch(Actions.toggleDoubleSizeMode());
dispatch(Actions.unsetFocus());
};
}
const ClutterBar = (props: StateProps & DispatchProps) => (
<div id="clutter-bar">
<ContextMenuTarget bottom handle={<div id="button-o" />}>
<OptionsContextMenu />
</ContextMenuTarget>
<div id="button-a" />
<div id="button-i" />
<div
title={"Toggle Doublesize Mode"}
id="button-d"
className={classnames({ selected: props.doubled })}
onMouseUp={props.handleMouseUp}
onMouseDown={props.handleMouseDown}
/>
<div id="button-v" />
</div>
);
const mapStateToProps = (state: AppState): StateProps => ({
doubled: state.display.doubled,
const ClutterBar = React.memo(() => {
const handleMouseDown = useActionCreator(setFocusDouble);
const handleMouseUp = useActionCreator(mouseUp);
const doubled = useTypedSelector(Selectors.getDoubled);
return (
<div id="clutter-bar">
<ContextMenuTarget bottom handle={<div id="button-o" />}>
<OptionsContextMenu />
</ContextMenuTarget>
<div id="button-a" />
<div id="button-i" />
<div
title={"Toggle Doublesize Mode"}
id="button-d"
className={classnames({ selected: doubled })}
onMouseUp={handleMouseUp}
onMouseDown={handleMouseDown}
/>
<div id="button-v" />
</div>
);
});
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
handleMouseDown: () => dispatch({ type: SET_FOCUS, input: "double" }),
handleMouseUp: () => {
dispatch(toggleDoubleSizeMode());
dispatch({ type: UNSET_FOCUS });
},
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ClutterBar);
export default ClutterBar;

View file

@ -1,22 +1,16 @@
import React from "react";
import { connect } from "react-redux";
import CharacterString from "../CharacterString";
import { AppState } from "../../types";
import * as Selectors from "../../selectors";
import { useTypedSelector } from "../../hooks";
interface StateProps {
kbps: string | null;
}
const Kbps = React.memo(() => {
const kbps = useTypedSelector(Selectors.getKbps);
return (
<div id="kbps">
<CharacterString>{kbps || ""}</CharacterString>
</div>
);
});
const Kbps = (props: StateProps) => (
<div id="kbps">
<CharacterString>{props.kbps || ""}</CharacterString>
</div>
);
function mapStateToProps(state: AppState): StateProps {
return { kbps: Selectors.getKbps(state) };
}
export default connect(mapStateToProps)(Kbps);
export default Kbps;

View file

@ -1,22 +1,16 @@
import React from "react";
import { connect } from "react-redux";
import CharacterString from "../CharacterString";
import { AppState } from "../../types";
import * as Selectors from "../../selectors";
import { useTypedSelector } from "../../hooks";
interface StateProps {
khz: string | null;
}
const Khz = React.memo(() => {
const khz = useTypedSelector(Selectors.getKhz);
return (
<div id="khz">
<CharacterString>{khz || ""}</CharacterString>
</div>
);
});
const Khz = (props: StateProps) => (
<div id="khz">
<CharacterString>{props.khz || ""}</CharacterString>
</div>
);
function mapStateToProps(state: AppState): StateProps {
return { khz: Selectors.getKhz(state) };
}
export default connect(mapStateToProps)(Khz);
export default Khz;

View file

@ -1,12 +1,8 @@
import React from "react";
import { connect } from "react-redux";
import Balance from "../Balance";
import { AppState } from "../../types";
interface StateProps {
balance: number;
}
import * as Selectors from "../../selectors";
import { useTypedSelector } from "../../hooks";
export const offsetFromBalance = (balance: number): number => {
const percent = Math.abs(balance) / 100;
@ -15,15 +11,14 @@ export const offsetFromBalance = (balance: number): number => {
return offset;
};
const MainBalance = (props: StateProps) => (
<Balance
id="balance"
style={{ backgroundPosition: `0 -${offsetFromBalance(props.balance)}px` }}
/>
);
const mapStateToProps = (state: AppState): StateProps => ({
balance: state.media.balance,
const MainBalance = React.memo(() => {
const balance = useTypedSelector(Selectors.getBalance);
return (
<Balance
id="balance"
style={{ backgroundPosition: `0 -${offsetFromBalance(balance)}px` }}
/>
);
});
export default connect(mapStateToProps)(MainBalance);
export default MainBalance;

View file

@ -1,16 +1,11 @@
import React from "react";
import { connect } from "react-redux";
import * as Selectors from "../../selectors";
import Volume from "../Volume";
import { AppState } from "../../types";
import { useTypedSelector } from "../../hooks";
interface Props {
volume: number;
}
const MainVolume = (props: Props) => {
const { volume } = props;
const MainVolume = React.memo(() => {
const volume = useTypedSelector(Selectors.getVolume);
const percent = volume / 100;
const sprite = Math.round(percent * 28);
const offset = (sprite - 1) * 15;
@ -23,10 +18,6 @@ const MainVolume = (props: Props) => {
<Volume />
</div>
);
};
const mapStateToProps = (state: AppState): Props => ({
volume: Selectors.getVolume(state),
});
export default connect(mapStateToProps)(MainVolume);
export default MainVolume;

View file

@ -1,22 +1,11 @@
import React from "react";
import { connect } from "react-redux";
import ClickedDiv from "../ClickedDiv";
import * as Actions from "../../actionCreators";
import { Dispatch } from "../../types";
import { useActionCreator } from "../../hooks";
interface Props {
minimize(): void;
}
const Minimize = ({ minimize }: Props) => (
<ClickedDiv id="minimize" title="Minimize" onClick={minimize} />
);
const mapDispatchToProps = (dispatch: Dispatch) => ({
minimize: () => dispatch(Actions.minimize()),
const Minimize = React.memo(() => {
const minimize = useActionCreator(Actions.minimize);
return <ClickedDiv id="minimize" title="Minimize" onClick={minimize} />;
});
export default connect(
null,
mapDispatchToProps
)(Minimize);
export default Minimize;

View file

@ -1,27 +1,16 @@
import React from "react";
import { connect } from "react-redux";
import classnames from "classnames";
import { AppState } from "../../types";
import * as Selectors from "../../selectors";
import { useTypedSelector } from "../../hooks";
interface Props {
channels: number | null;
}
const MonoStereo = React.memo(() => {
const channels = useTypedSelector(Selectors.getChannels);
return (
<div className="mono-stereo">
<div id="stereo" className={classnames({ selected: channels === 2 })} />
<div id="mono" className={classnames({ selected: channels === 1 })} />
</div>
);
});
const MonoStereo = (props: Props) => (
<div className="mono-stereo">
<div
id="stereo"
className={classnames({ selected: props.channels === 2 })}
/>
<div id="mono" className={classnames({ selected: props.channels === 1 })} />
</div>
);
const mapStateToProps = (state: AppState): Props => {
return {
channels: Selectors.getChannels(state),
};
};
export default connect(mapStateToProps)(MonoStereo);
export default MonoStereo;

View file

@ -1,28 +1,25 @@
import React from "react";
import { connect } from "react-redux";
import classnames from "classnames";
import { getWindowOpen } from "../../selectors";
import { toggleWindow } from "../../actionCreators";
import * as Selectors from "../../selectors";
import * as Actions from "../../actionCreators";
import { useTypedSelector, useActionCreator } from "../../hooks";
const PlaylistToggleButton = props => (
<div
id="playlist-button"
className={classnames({ selected: props.selected })}
onClick={props.handleClick}
title="Toggle Playlist Editor"
/>
);
function togglePlaylist() {
return Actions.toggleWindow("playlist");
}
const mapStateToProps = state => ({
selected: getWindowOpen(state)("playlist"),
const PlaylistToggleButton = React.memo(() => {
const selected = useTypedSelector(Selectors.getWindowOpen)("playlist");
const handleClick = useActionCreator(togglePlaylist);
return (
<div
id="playlist-button"
className={classnames({ selected })}
onClick={handleClick}
title="Toggle Playlist Editor"
/>
);
});
const mapDispatchToProps = {
handleClick: () => toggleWindow("playlist"),
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(PlaylistToggleButton);
export default PlaylistToggleButton;

View file

@ -1,5 +1,7 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import * as Utils from "./utils";
import { Action, Thunk, AppState } from "./types";
interface Size {
width: number;
@ -25,3 +27,18 @@ export function useWindowSize() {
}, [setSize, handler]);
return size;
}
export function useActionCreator<T extends (...args: any[]) => Action | Thunk>(
actionCreator: T
): (...funcArgs: Parameters<T>) => void {
const dispatch = useDispatch();
return useCallback((...args) => dispatch(actionCreator(...args)), [
dispatch,
actionCreator,
]);
}
// TODO: Return useSelector directly and apply the type without wrapping
export function useTypedSelector<T>(selector: (state: AppState) => T): T {
return useSelector(selector);
}

View file

@ -138,14 +138,6 @@ describe("can serialize", () => {
expected: false,
});
testSerialization({
name: "equalizer auto",
// @ts-ignore
action: Actions.toggleEqAuto(),
selector: Selectors.getEqualizerAuto,
expected: true,
});
testSerialization({
name: "equalizer band",
action: Actions.setEqBand(60, 100),

View file

@ -72,7 +72,7 @@
"@types/rc-slider": "^8.6.3",
"@types/react": "^16.8.13",
"@types/react-dom": "^16.8.4",
"@types/react-redux": "^7.0.6",
"@types/react-redux": "^7.1.1",
"@types/webaudioapi": "^0.0.27",
"@typescript-eslint/eslint-plugin": "^1.4.2",
"@typescript-eslint/parser": "^1.4.2",
@ -119,7 +119,7 @@
"rc-slider": "^8.6.9",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-redux": "^7.1.0-alpha.4",
"react-redux": "^7.1.0",
"react-test-renderer": "^16.8.1",
"redux": "^4.0.1",
"redux-devtools-extension": "^2.13.2",

View file

@ -644,10 +644,10 @@
dependencies:
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.4.3":
version "7.4.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.3.tgz#79888e452034223ad9609187a0ad1fe0d2ad4bdc"
integrity sha512-9lsJwJLxDh/T3Q3SZszfWOTkk3pHbkmH+3KY+zwIDmsNlxsumuhS2TH3NIpktU4kNvfzy+k3eLT7aTJSPTo0OA==
"@babel/runtime@^7.4.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
dependencies:
regenerator-runtime "^0.13.2"
@ -994,13 +994,14 @@
dependencies:
"@types/react" "*"
"@types/react-redux@^7.0.6":
version "7.0.6"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.0.6.tgz#992271450e0d3bf61130ad9e356ad018841c7f78"
integrity sha512-Nlofk/xq8oVWpylvrFayezNb/HONsYJfjlSmTmZ7xoMDe+Muf6c1qHMVRZ7C5S2W1+iVcY21ggZwlUgLv+66hQ==
"@types/react-redux@^7.1.1":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.1.tgz#eb01e89cf71cad77df9f442b819d5db692b997cb"
integrity sha512-owqNahzE8en/jR4NtrUJDJya3tKru7CIEGSRL/pVS84LtSCdSoT7qZTkrbBd3S4Lp11sAp+7LsvxIeONJVKMnw==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react@*":
@ -8739,12 +8740,12 @@ react-is@^16.8.4, react-is@^16.8.6:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
react-redux@^7.1.0-alpha.4:
version "7.1.0-alpha.4"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.0-alpha.4.tgz#103bab23e336ce18951bc76a8c9567ce0607fc44"
integrity sha512-Vxk6F1ibo2EC/cThTVUbvhTyltUFg7iLnEtro+QwOfOCygKpvwUtag1a8MPWPGv+BLWMXHniOaeo0sy6INhzFg==
react-redux@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.0.tgz#72af7cf490a74acdc516ea9c1dd80e25af9ea0b2"
integrity sha512-hyu/PoFK3vZgdLTg9ozbt7WF3GgX5+Yn3pZm5/96/o4UueXA+zj08aiSC9Mfj2WtD1bvpIb3C5yvskzZySzzaw==
dependencies:
"@babel/runtime" "^7.4.3"
"@babel/runtime" "^7.4.5"
hoist-non-react-statics "^3.3.0"
invariant "^2.2.4"
loose-envify "^1.4.0"