refactor(hooks): here be dragons!

This commit is contained in:
haishanh 2018-11-02 19:19:38 +08:00 committed by Haishan
parent 7f75345c03
commit e3afe5ff90
17 changed files with 389 additions and 390 deletions

3
.gitignore vendored
View file

@ -1,7 +1,8 @@
*.*~
.DS_Store
node_modules
node_modules/
public/
experimental/
deploy_ghpages.sh
tags
*.log

View file

@ -39,7 +39,7 @@
"memoize-one": "^4.0.2",
"modern-normalize": "^0.5.0",
"prop-types": "^15.5.10",
"react": "^16.7.0-alpha.0",
"react": "16.7.0-alpha.0",
"react-cache": "^2.0.0-alpha.0",
"react-dom": "^16.7.0-alpha.0",
"react-modal": "^3.6.1",

View file

@ -53,7 +53,8 @@ const store = {
function pump(reader) {
return reader.read().then(({ done, value }) => {
if (done) {
console.log('done');
// eslint-disable-next-line no-console
console.log('GET /logs streaming done');
return;
}
const t = textDecoder.decode(value);
@ -62,6 +63,7 @@ function pump(reader) {
try {
store.appendData(JSON.parse(s));
} catch (err) {
// eslint-disable-next-line no-console
console.log('JSON.parse error', JSON.parse(s));
}
});
@ -80,7 +82,8 @@ function fetchLogs() {
})
.catch(err => {
store.fetched = false;
console.log('Error', err);
// eslint-disable-next-line no-console
console.log('GET /logs error', err);
});
return store;
}

View file

@ -54,7 +54,8 @@ let fetched = false;
function pump(reader) {
return reader.read().then(({ done, value }) => {
if (done) {
console.log('done');
// eslint-disable-next-line no-console
console.log('GET /traffic streaming done');
fetched = false;
return;
}

View file

@ -16,4 +16,5 @@ root.render(<Root />);
// };
// render(Root, props);
// eslint-disable-next-line no-console
console.log('Checkout the repo: https://github.com/haishanh/yacd');

View file

@ -1,78 +1,65 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import React, { useState, useEffect, useRef } from 'react';
import { useComponentState, useActions } from 'm/store';
import Input from 'c/Input';
import Button from 'c/Button';
import s0 from './APIConfig.module.scss';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { getClashAPIConfig, updateClashAPIConfig } from 'd/app';
const mapStateToProps = s => {
const apiConfig = getClashAPIConfig(s);
return { apiConfig };
};
const mapStateToProps = s => ({
apiConfig: getClashAPIConfig(s)
});
const mapDispatchToProps = dispatch => {
return {
updateClashAPIConfig: bindActionCreators(updateClashAPIConfig, dispatch)
};
};
function APIConfig2() {
const { apiConfig } = useComponentState(mapStateToProps);
const [hostname, setHostname] = useState(apiConfig.hostname);
const [port, setPort] = useState(apiConfig.port);
const [secret, setSecret] = useState(apiConfig.secret);
const actions = useActions({ updateClashAPIConfig });
class APIConfig extends Component {
static propTypes = {
apiConfig: PropTypes.object.isRequired,
updateClashAPIConfig: PropTypes.func.isRequired
};
const contentEl = useRef(null);
useEffect(() => {
contentEl.current.focus();
}, []);
state = {
hostname: this.props.apiConfig.hostname,
port: this.props.apiConfig.port,
secret: this.props.apiConfig.secret
};
componentDidMount() {
this.content.focus();
}
handleInputOnChange = e => {
const handleInputOnChange = e => {
const target = e.target;
const { name } = target;
let value = target.value.trim();
let value;
if (name === 'port') {
if (Number(target.value) < 0 || Number(target.value) > 65535) return;
}
value = target.value.trim();
if (value === '') return;
this.setState({ [name]: value });
switch (name) {
case 'port':
if (Number(value) < 0 || Number(value) > 65535) return;
setPort(value);
break;
case 'hostname':
setHostname(value);
break;
case 'secret':
setSecret(value);
break;
}
};
updateClashAPIConfig() {
const { hostname, port, secret } = this.state;
this.props.updateClashAPIConfig({ hostname, port, secret });
function updateConfig() {
actions.updateClashAPIConfig({ hostname, port, secret });
}
handleConfirmOnClick = () => {
this.updateClashAPIConfig();
};
handleContentOnKeyDown = e => {
function handleContentOnKeyDown(e) {
// enter keyCode is 13
if (e.keyCode !== 13) return;
this.updateClashAPIConfig();
};
updateConfig();
}
render() {
const { hostname, port, secret } = this.state;
return (
<div
className={s0.root}
ref={e => (this.content = e)}
ref={contentEl}
tabIndex="1"
onKeyDown={this.handleContentOnKeyDown}
onKeyDown={handleContentOnKeyDown}
>
<div className={s0.header}>RESTful API config for Clash</div>
<div className={s0.body}>
@ -84,14 +71,14 @@ class APIConfig extends Component {
name="hostname"
placeholder="Hostname"
value={hostname}
onChange={this.handleInputOnChange}
onChange={handleInputOnChange}
/>
<Input
type="number"
name="port"
placeholder="Port"
value={port}
onChange={this.handleInputOnChange}
onChange={handleInputOnChange}
/>
</div>
</div>
@ -103,20 +90,16 @@ class APIConfig extends Component {
name="secret"
value={secret}
placeholder="Optional"
onChange={this.handleInputOnChange}
onChange={handleInputOnChange}
/>
</div>
</div>
</div>
<div className={s0.footer}>
<Button label="Confirm" onClick={this.handleConfirmOnClick} />
<Button label="Confirm" onClick={updateConfig} />
</div>
</div>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(APIConfig);
export default APIConfig2;

View file

@ -1,39 +1,28 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import React, { useEffect } from 'react';
import { useActions, useComponentState } from 'm/store';
import Modal from 'c/Modal';
import APIConfig from 'c/APIConfig';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { closeModal } from 'd/modals';
import { fetchConfigs } from 'd/configs';
const mapStateToProps = state => {
const modals = state.modals;
return { modals };
const mapStateToProps = s => ({
modals: s.modals
});
const actions = {
closeModal,
fetchConfigs
};
const mapDispatchToProps = dispatch => {
return {
closeModal: bindActionCreators(closeModal, dispatch),
fetchConfigs: bindActionCreators(fetchConfigs, dispatch)
};
};
export default function APIDiscovery() {
const { modals } = useComponentState(mapStateToProps);
const { closeModal, fetchConfigs } = useActions(actions);
useEffect(() => {
fetchConfigs();
}, []);
class APIDiscovery extends Component {
static propTypes = {
closeModal: PropTypes.func,
fetchConfigs: PropTypes.func,
modals: PropTypes.object
};
componentDidMount() {
this.props.fetchConfigs();
}
render() {
const { modals, closeModal } = this.props;
return (
<Modal
isOpen={modals.apiConfig}
@ -45,9 +34,3 @@ class APIDiscovery extends Component {
</Modal>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(APIDiscovery);

View file

@ -1,12 +1,9 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import React, { useEffect } from 'react';
import { useComponentState, useActions } from 'm/store';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { getConfigs, fetchConfigs, updateConfigs } from 'd/configs';
import ContentHeader from 'c/ContentHeader';
import Switch from 'c/Switch';
import ToggleSwitch from 'c/ToggleSwitch';
import Input from 'c/Input';
@ -51,29 +48,16 @@ const actions = {
updateConfigs
};
const mapStateToProps = s => {
return {
configs: getConfigs(s)
};
};
const mapStateToProps = s => ({ configs: getConfigs(s) });
const mapDispatchToProps = dispatch => {
return bindActionCreators(actions, dispatch);
};
export default function Config2() {
const { fetchConfigs, updateConfigs } = useActions(actions);
const { configs } = useComponentState(mapStateToProps);
useEffect(() => {
fetchConfigs();
}, []);
class Config extends Component {
static propTypes = {
configs: PropTypes.object,
fetchConfigs: PropTypes.func,
updateConfigs: PropTypes.func
};
componentDidMount() {
this.props.fetchConfigs();
}
handleInputOnChange = ev => {
const { configs } = this.props;
function handleInputOnChange(ev) {
const target = ev.target;
const { name } = target;
@ -89,11 +73,9 @@ class Config extends Component {
value = target.value;
}
if (configs[name] === value) return;
this.props.updateConfigs({ [name]: value });
};
updateConfigs({ [name]: value });
}
render() {
const { configs } = this.props;
return (
<div>
<ContentHeader title="Config" />
@ -103,7 +85,7 @@ class Config extends Component {
<Input
name="port"
value={configs.port}
onChange={this.handleInputOnChange}
onChange={handleInputOnChange}
/>
</div>
@ -112,7 +94,7 @@ class Config extends Component {
<Input
name="socket-port"
value={configs['socket-port']}
onChange={this.handleInputOnChange}
onChange={handleInputOnChange}
/>
</div>
@ -121,7 +103,7 @@ class Config extends Component {
<Input
name="redir-port"
value={configs['redir-port']}
onChange={this.handleInputOnChange}
onChange={handleInputOnChange}
/>
</div>
@ -130,7 +112,7 @@ class Config extends Component {
<Switch
name="allow-lan"
checked={configs['allow-lan']}
onChange={this.handleInputOnChange}
onChange={handleInputOnChange}
/>
</div>
@ -140,7 +122,7 @@ class Config extends Component {
options={optionsRule}
name="mode"
value={configs.mode}
onChange={this.handleInputOnChange}
onChange={handleInputOnChange}
/>
</div>
@ -150,16 +132,10 @@ class Config extends Component {
options={optionsLogLevel}
name="log-level"
value={configs['log-level']}
onChange={this.handleInputOnChange}
onChange={handleInputOnChange}
/>
</div>
</div>
</div>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Config);

View file

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import React, { useEffect } from 'react';
import { useActions, useComponentState } from 'm/store';
import ContentHeader from 'c/ContentHeader';
import ProxyGroup from 'c/ProxyGroup';
@ -7,8 +7,6 @@ import Button from 'c/Button';
import s0 from 'c/Proxies.module.scss';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {
getUserProxies,
getProxyGroupNames,
@ -16,33 +14,23 @@ import {
requestDelayAll
} from 'd/proxies';
function mapStateToProps(s) {
return {
const mapStateToProps = s => ({
proxies: getUserProxies(s),
groupNames: getProxyGroupNames(s)
};
}
});
function mapDispatchToProps(dispatch) {
return {
fetchProxies: bindActionCreators(fetchProxies, dispatch),
requestDelayAll: bindActionCreators(requestDelayAll, dispatch)
};
}
class Proxies extends Component {
static propTypes = {
groupNames: PropTypes.array.isRequired,
fetchProxies: PropTypes.func.isRequired,
requestDelayAll: PropTypes.func.isRequired
const actions = {
fetchProxies,
requestDelayAll
};
componentDidMount() {
this.props.fetchProxies();
}
export default function Proxies() {
const { fetchProxies, requestDelayAll } = useActions(actions);
useEffect(() => {
fetchProxies();
}, []);
const { groupNames } = useComponentState(mapStateToProps);
render() {
const { groupNames, requestDelayAll } = this.props;
return (
<div>
<ContentHeader title="Proxies" />
@ -61,9 +49,3 @@ class Proxies extends Component {
</div>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Proxies);

View file

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { useComponentState } from 'm/store';
import Icon from 'c/Icon';
import ProxyLatency from 'c/ProxyLatency';
@ -12,7 +13,6 @@ import fallback from 's/fallback.svg';
import s0 from './Proxy.module.scss';
import { connect } from 'react-redux';
import { getDelay, getUserProxies } from 'd/proxies';
const colors = {
@ -40,18 +40,10 @@ const mapStateToProps = s => {
};
};
const mapDispatchToProps = null;
function Proxy({ now, name }) {
const { proxies, delay } = useComponentState(mapStateToProps);
class Proxy extends Component {
static propTypes = {
now: PropTypes.bool,
delay: PropTypes.object,
proxies: PropTypes.object,
name: PropTypes.string
};
render() {
const { name, proxies, delay, now } = this.props;
// const { name, proxies, delay, now } = this.props;
const latency = delay[name];
const proxy = proxies[name];
const color = now ? colors[proxy.type] : '#555';
@ -69,9 +61,11 @@ class Proxy extends Component {
</div>
);
}
}
Proxy.propTypes = {
now: PropTypes.bool,
delay: PropTypes.object,
proxies: PropTypes.object,
name: PropTypes.string
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Proxy);
export default Proxy;

View file

@ -1,53 +1,37 @@
import React, { Component } from 'react';
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import memoize from 'memoize-one';
import { useActions, useComponentState } from 'm/store';
import Proxy from 'c/Proxy';
import s0 from './ProxyGroup.module.scss';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { getUserProxies, switchProxy } from 'd/proxies';
const mapStateToProps = s => {
return {
const mapStateToProps = s => ({
proxies: getUserProxies(s)
};
};
const mapDispatchToProps = dispatch => {
return {
switchProxy: bindActionCreators(switchProxy, dispatch)
};
};
});
// should move this to sth like constants.js
// const userProxyTypes = ['Shadowsocks', 'Vmess', 'Socks5'];
class ProxyGroup extends Component {
static propTypes = {
// group name
name: PropTypes.string.isRequired,
proxies: PropTypes.object,
switchProxy: PropTypes.func
};
reOrderProxies = memoize((list, now) => {
const a = [now];
list.forEach(i => i !== now && a.push(i));
return a;
});
render() {
const { name, proxies, switchProxy } = this.props;
export default function ProxyGroup2({ name }) {
const { proxies } = useComponentState(mapStateToProps);
const actions = useActions({ switchProxy });
const group = proxies[name];
let list;
if (group.all) {
list = this.reOrderProxies(group.all, group.now);
const { all, now } = group;
const list = useMemo(
() => {
if (all) {
const a = [now];
all.forEach(i => i !== now && a.push(i));
return a;
} else {
list = [group.now];
return [now];
}
},
[all, now]
);
return (
<div className={s0.group}>
<div className={s0.header}>
@ -62,7 +46,7 @@ class ProxyGroup extends Component {
<div
className={s0.proxy}
key={proxyName}
onClick={() => switchProxy(name, proxyName)}
onClick={() => actions.switchProxy(name, proxyName)}
>
<Proxy name={proxyName} now={proxyName === group.now} />
</div>
@ -72,9 +56,7 @@ class ProxyGroup extends Component {
</div>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProxyGroup);
ProxyGroup2.propTypes = {
name: PropTypes.string
};

View file

@ -1,5 +1,6 @@
import React from 'react';
import { Provider } from 'react-redux';
import { Provider as StoreProvider } from 'm/store';
import { HashRouter as Router, Route } from 'react-router-dom';
// import { hot } from 'react-hot-loader';
// import createHistory from 'history/createHashHistory';
@ -21,8 +22,11 @@ import { store } from '../store/configureStore';
import './Root.scss';
import s0 from './Root.module.scss';
window.store = store;
const Root = () => (
<Provider store={store}>
<StoreProvider store={store}>
<Router>
<div className={s0.app}>
<APIDiscovery />
@ -36,6 +40,7 @@ const Root = () => (
</div>
</div>
</Router>
</StoreProvider>
</Provider>
);
// <Route exact path="/__0" component={StyleGuide} />

View file

@ -18,6 +18,7 @@ export function fetchConfigs() {
res = await configsAPI.fetchConfigs();
} catch (err) {
// FIXME
// eslint-disable-next-line no-console
console.log('Error fetch configs', err);
dispatch(openModal('apiConfig'));
return;
@ -27,6 +28,7 @@ export function fetchConfigs() {
if (res.status === 404 || res.status === 401) {
dispatch(openModal('apiConfig'));
} else {
// eslint-disable-next-line no-console
console.log('Error fetch configs', res.statusText);
}
return;
@ -57,10 +59,12 @@ export function updateConfigs(partialConfg) {
.then(
res => {
if (res.ok === false) {
// eslint-disable-next-line no-console
console.log('Error update configs', res.statusText);
}
},
err => {
// eslint-disable-next-line no-console
console.log('Error update configs', err);
throw err;
}

View file

@ -69,10 +69,12 @@ export function switchProxy(name1, name2) {
.then(
res => {
if (res.ok === false) {
// eslint-disable-next-line no-console
console.log('failed to swith proxy', res.statusText);
}
},
err => {
// eslint-disable-next-line no-console
console.log(err, 'failed to swith proxy');
}
)

35
src/misc/shallowEqual.js Normal file
View file

@ -0,0 +1,35 @@
const hasOwn = Object.prototype.hasOwnProperty;
function is(x, y) {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
return x !== x && y !== y;
}
}
export default function shallowEqual(objA, objB) {
if (is(objA, objB)) return true;
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) return false;
for (let i = 0; i < keysA.length; i++) {
if (!hasOwn.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
return false;
}
}
return true;
}

47
src/misc/store.js Normal file
View file

@ -0,0 +1,47 @@
import React, { createContext, useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import shallowEqual from './shallowEqual';
export const StoreContext = createContext(null);
export function Provider({ store, children }) {
return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
}
Provider.propTypes = {
store: PropTypes.object,
children: PropTypes.node
};
export function useStore() {
// return the context
// which is the redux store
return useContext(StoreContext);
}
export function useActions(actions) {
const { dispatch } = useStore();
const a = typeof actions === 'function' ? actions() : actions;
return bindActionCreators(a, dispatch);
}
export function useComponentState(selector) {
const store = useStore();
const initialMappedState = selector(store.getState());
const [compState, setCompState] = useState(initialMappedState);
// subscribe to store change
useEffect(() => {
let compStateCurr = compState;
return store.subscribe(() => {
const compStateNext = selector(store.getState());
if (shallowEqual(compStateCurr, compStateNext)) return;
// update state if not equal
compStateCurr = compStateNext;
setCompState(compStateNext);
});
}, []);
return compState;
}

View file

@ -6692,7 +6692,7 @@ react-router@^4.4.0-beta.4:
path-to-regexp "^1.7.0"
warning "^4.0.1"
react@^16.7.0-alpha.0:
react@16.7.0-alpha.0:
version "16.7.0-alpha.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.7.0-alpha.0.tgz#e2ed4abe6f268c9b092a1d1e572953684d1783a9"
integrity sha512-V0za4H01aoAF0SdzahHepvfvzTQ1xxkgMX4z8uKzn+wzZAlVk0IVpleqyxZWluqmdftNedj6fIIZRO/rVYVFvQ==