refactor: Remove old react lifecycle methods (#1480)

* refactor: Remove deprecated APIs

* bump mobx-react for hooks support

* inject -> useStores
https://mobx-react.js.org/recipes-migration\#hooks-to-the-rescue

* chore: React rules of hooks lint
This commit is contained in:
Tom Moor 2020-08-23 11:51:56 -07:00 committed by GitHub
parent 179176c312
commit ec38f5d79c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 193 additions and 177 deletions

View File

@ -4,7 +4,8 @@
"react-app", "react-app",
"plugin:import/errors", "plugin:import/errors",
"plugin:import/warnings", "plugin:import/warnings",
"plugin:flowtype/recommended" "plugin:flowtype/recommended",
"plugin:react-hooks/recommended"
], ],
"plugins": [ "plugins": [
"prettier", "prettier",

View File

@ -14,7 +14,7 @@ export default function DelayedMount({ delay = 250, children }: Props) {
return () => { return () => {
clearTimeout(timeout); clearTimeout(timeout);
}; };
}, []); }, [delay]);
if (!isShowing) { if (!isShowing) {
return null; return null;

View File

@ -20,12 +20,7 @@ type Props = {
onClose: () => void, onClose: () => void,
}; };
function HoverPreview({ node, documents, onClose, event }: Props) { function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
// previews only work for internal doc links for now
if (!isInternalUrl(node.href)) {
return null;
}
const slug = parseDocumentSlugFromUrl(node.href); const slug = parseDocumentSlugFromUrl(node.href);
const [isVisible, setVisible] = React.useState(false); const [isVisible, setVisible] = React.useState(false);
@ -131,6 +126,15 @@ function HoverPreview({ node, documents, onClose, event }: Props) {
); );
} }
function HoverPreview({ node, ...rest }: Props) {
// previews only work for internal doc links for now
if (!isInternalUrl(node.href)) {
return null;
}
return <HoverPreviewInternal {...rest} node={node} />;
}
const Animate = styled.div` const Animate = styled.div`
animation: ${fadeAndSlideIn} 150ms ease; animation: ${fadeAndSlideIn} 150ms ease;

View File

@ -44,21 +44,22 @@ class Layout extends React.Component<Props> {
@observable redirectTo: ?string; @observable redirectTo: ?string;
@observable keyboardShortcutsOpen: boolean = false; @observable keyboardShortcutsOpen: boolean = false;
componentWillMount() { constructor(props) {
this.updateBackground(); super();
this.updateBackground(props);
} }
componentDidUpdate() { componentDidUpdate() {
this.updateBackground(); this.updateBackground(this.props);
if (this.redirectTo) { if (this.redirectTo) {
this.redirectTo = undefined; this.redirectTo = undefined;
} }
} }
updateBackground() { updateBackground(props) {
// ensure the wider page color always matches the theme // ensure the wider page color always matches the theme
window.document.body.style.background = this.props.theme.background; window.document.body.style.background = props.theme.background;
} }
@keydown("shift+/") @keydown("shift+/")

View File

@ -17,7 +17,8 @@ class Mask extends React.Component<Props> {
return false; return false;
} }
componentWillMount() { constructor() {
super();
this.width = randomInteger(75, 100); this.width = randomInteger(75, 100);
} }

View File

@ -1,65 +1,60 @@
// @flow // @flow
import { observer, inject } from "mobx-react"; import { observer } from "mobx-react";
import { CloseIcon, MenuIcon } from "outline-icons"; import { CloseIcon, MenuIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { withRouter } from "react-router-dom"; import { withRouter } from "react-router-dom";
import type { Location } from "react-router-dom"; import type { Location } from "react-router-dom";
import styled from "styled-components"; import styled from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import UiStore from "stores/UiStore";
import Fade from "components/Fade"; import Fade from "components/Fade";
import Flex from "components/Flex"; import Flex from "components/Flex";
import usePrevious from "hooks/usePrevious";
import useStores from "hooks/useStores";
let firstRender = true; let firstRender = true;
type Props = { type Props = {
children: React.Node, children: React.Node,
location: Location, location: Location,
ui: UiStore,
}; };
@observer function Sidebar({ location, children }: Props) {
class Sidebar extends React.Component<Props> { const { ui } = useStores();
componentWillReceiveProps = (nextProps: Props) => { const previousLocation = usePrevious(location);
if (this.props.location !== nextProps.location) {
this.props.ui.hideMobileSidebar(); React.useEffect(() => {
if (location !== previousLocation) {
ui.hideMobileSidebar();
} }
}; }, [ui, location]);
toggleSidebar = () => { const content = (
this.props.ui.toggleMobileSidebar(); <Container
}; editMode={ui.editMode}
mobileSidebarVisible={ui.mobileSidebarVisible}
render() { column
const { children, ui } = this.props; >
const content = ( <Toggle
<Container onClick={ui.toggleMobileSidebar}
editMode={ui.editMode}
mobileSidebarVisible={ui.mobileSidebarVisible} mobileSidebarVisible={ui.mobileSidebarVisible}
column
> >
<Toggle {ui.mobileSidebarVisible ? (
onClick={this.toggleSidebar} <CloseIcon size={32} />
mobileSidebarVisible={ui.mobileSidebarVisible} ) : (
> <MenuIcon size={32} />
{ui.mobileSidebarVisible ? ( )}
<CloseIcon size={32} /> </Toggle>
) : ( {children}
<MenuIcon size={32} /> </Container>
)} );
</Toggle>
{children}
</Container>
);
// Fade in the sidebar on first render after page load // Fade in the sidebar on first render after page load
if (firstRender) { if (firstRender) {
firstRender = false; firstRender = false;
return <Fade>{content}</Fade>; return <Fade>{content}</Fade>;
}
return content;
} }
return content;
} }
const Container = styled(Flex)` const Container = styled(Flex)`
@ -117,4 +112,4 @@ const Toggle = styled.a`
`}; `};
`; `;
export default withRouter(inject("ui")(Sidebar)); export default withRouter(observer(Sidebar));

View File

@ -1,5 +1,4 @@
// @flow // @flow
import { observable, action } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { CollapsedIcon } from "outline-icons"; import { CollapsedIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
@ -25,79 +24,80 @@ type Props = {
depth?: number, depth?: number,
}; };
@observer function SidebarLink({
class SidebarLink extends React.Component<Props> { icon,
@observable expanded: ?boolean = this.props.expanded; children,
onClick,
to,
label,
active,
menu,
menuOpen,
hideDisclosure,
theme,
exact,
href,
depth,
...rest
}: Props) {
const [expanded, setExpanded] = React.useState(rest.expanded);
style = { const style = React.useMemo(() => {
paddingLeft: `${(this.props.depth || 0) * 16 + 16}px`, return {
}; paddingLeft: `${(depth || 0) * 16 + 16}px`,
componentWillReceiveProps(nextProps: Props) {
if (nextProps.expanded !== undefined) {
this.expanded = nextProps.expanded;
}
}
@action
handleClick = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.expanded = !this.expanded;
};
@action
handleExpand = () => {
this.expanded = true;
};
render() {
const {
icon,
children,
onClick,
to,
label,
active,
menu,
menuOpen,
hideDisclosure,
exact,
href,
} = this.props;
const showDisclosure = !!children && !hideDisclosure;
const activeStyle = {
color: this.props.theme.text,
background: this.props.theme.sidebarItemBackground,
fontWeight: 600,
...this.style,
}; };
}, [depth]);
return ( React.useEffect(() => {
<Wrapper column> if (rest.expanded) {
<StyledNavLink setExpanded(rest.expanded);
activeStyle={activeStyle} }
style={active ? activeStyle : this.style} }, [rest.expanded]);
onClick={onClick}
exact={exact !== false} const handleClick = React.useCallback(
to={to} (ev: SyntheticEvent<>) => {
as={to ? undefined : href ? "a" : "div"} ev.preventDefault();
href={href} ev.stopPropagation();
> setExpanded(!expanded);
{icon && <IconWrapper>{icon}</IconWrapper>} },
<Label onClick={this.handleExpand}> [expanded]
{showDisclosure && ( );
<Disclosure expanded={this.expanded} onClick={this.handleClick} />
)} const handleExpand = React.useCallback(() => {
{label} setExpanded(true);
</Label> }, []);
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
</StyledNavLink> const showDisclosure = !!children && !hideDisclosure;
{this.expanded && children} const activeStyle = {
</Wrapper> color: theme.text,
); background: theme.sidebarItemBackground,
} fontWeight: 600,
...style,
};
return (
<Wrapper column>
<StyledNavLink
activeStyle={activeStyle}
style={active ? activeStyle : style}
onClick={onClick}
exact={exact !== false}
to={to}
as={to ? undefined : href ? "a" : "div"}
href={href}
>
{icon && <IconWrapper>{icon}</IconWrapper>}
<Label onClick={handleExpand}>
{showDisclosure && (
<Disclosure expanded={expanded} onClick={handleClick} />
)}
{label}
</Label>
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
</StyledNavLink>
{expanded && children}
</Wrapper>
);
} }
// accounts for whitespace around icon // accounts for whitespace around icon
@ -171,4 +171,4 @@ const Disclosure = styled(CollapsedIcon)`
${({ expanded }) => !expanded && "transform: rotate(-90deg);"}; ${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
`; `;
export default withRouter(withTheme(SidebarLink)); export default withRouter(withTheme(observer(SidebarLink)));

10
app/hooks/usePrevious.js Normal file
View File

@ -0,0 +1,10 @@
// @flow
import * as React from "react";
export default function usePrevious(value: any) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
}

8
app/hooks/useStores.js Normal file
View File

@ -0,0 +1,8 @@
// @flow
import { MobXProviderContext } from "mobx-react";
import * as React from "react";
import RootStore from "stores";
export default function useStores(): typeof RootStore {
return React.useContext(MobXProviderContext);
}

View File

@ -12,32 +12,24 @@ import Toasts from "components/Toasts";
import Routes from "./routes"; import Routes from "./routes";
import env from "env"; import env from "env";
let DevTools;
if (process.env.NODE_ENV !== "production") {
DevTools = require("mobx-react-devtools").default; // eslint-disable-line global-require
}
const element = document.getElementById("root"); const element = document.getElementById("root");
if (element) { if (element) {
render( render(
<> <ErrorBoundary>
<ErrorBoundary> <Provider {...stores}>
<Provider {...stores}> <Theme>
<Theme> <Router>
<Router> <>
<> <ScrollToTop>
<ScrollToTop> <Routes />
<Routes /> </ScrollToTop>
</ScrollToTop> <Toasts />
<Toasts /> </>
</> </Router>
</Router> </Theme>
</Theme> </Provider>
</Provider> </ErrorBoundary>,
</ErrorBoundary>
{DevTools && <DevTools position={{ bottom: 0, right: 0 }} />}
</>,
element element
); );
} }

View File

@ -62,10 +62,10 @@ class CollectionScene extends React.Component<Props> {
} }
} }
componentWillReceiveProps(nextProps) { componentDidUpdate(prevProps) {
const { id } = nextProps.match.params; const { id } = this.props.match.params;
if (id && id !== this.props.match.params.id) { if (id && id !== prevProps.match.params.id) {
this.loadContent(id); this.loadContent(id);
} }
} }

View File

@ -109,34 +109,34 @@ class UiStore {
}; };
@action @action
enableEditMode() { enableEditMode = () => {
this.editMode = true; this.editMode = true;
} };
@action @action
disableEditMode() { disableEditMode = () => {
this.editMode = false; this.editMode = false;
} };
@action @action
enableProgressBar() { enableProgressBar = () => {
this.progressBarVisible = true; this.progressBarVisible = true;
} };
@action @action
disableProgressBar() { disableProgressBar = () => {
this.progressBarVisible = false; this.progressBarVisible = false;
} };
@action @action
toggleMobileSidebar() { toggleMobileSidebar = () => {
this.mobileSidebarVisible = !this.mobileSidebarVisible; this.mobileSidebarVisible = !this.mobileSidebarVisible;
} };
@action @action
hideMobileSidebar() { hideMobileSidebar = () => {
this.mobileSidebarVisible = false; this.mobileSidebarVisible = false;
} };
@action @action
showToast = ( showToast = (

View File

@ -115,7 +115,7 @@
"koa-static": "^4.0.1", "koa-static": "^4.0.1",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"mobx": "4.6.0", "mobx": "4.6.0",
"mobx-react": "^5.4.2", "mobx-react": "^6.2.5",
"natural-sort": "^1.0.0", "natural-sort": "^1.0.0",
"nodemailer": "^4.4.0", "nodemailer": "^4.4.0",
"outline-icons": "^1.21.0-6", "outline-icons": "^1.21.0-6",
@ -170,13 +170,13 @@
"eslint-plugin-jsx-a11y": "^6.1.0", "eslint-plugin-jsx-a11y": "^6.1.0",
"eslint-plugin-prettier": "^3.1.0", "eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-react": "^7.20.0", "eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.1.0",
"fetch-test-server": "^1.1.0", "fetch-test-server": "^1.1.0",
"flow-bin": "^0.104.0", "flow-bin": "^0.104.0",
"html-webpack-plugin": "3.2.0", "html-webpack-plugin": "3.2.0",
"jest-cli": "^26.0.0", "jest-cli": "^26.0.0",
"koa-webpack-dev-middleware": "^1.4.5", "koa-webpack-dev-middleware": "^1.4.5",
"koa-webpack-hot-middleware": "^1.0.3", "koa-webpack-hot-middleware": "^1.0.3",
"mobx-react-devtools": "^6.0.3",
"nodemon": "^1.19.4", "nodemon": "^1.19.4",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"rimraf": "^2.5.4", "rimraf": "^2.5.4",
@ -191,4 +191,4 @@
"js-yaml": "^3.13.1" "js-yaml": "^3.13.1"
}, },
"version": "0.46.0" "version": "0.46.0"
} }

View File

@ -4383,6 +4383,11 @@ eslint-plugin-prettier@^3.1.0:
dependencies: dependencies:
prettier-linter-helpers "^1.0.0" prettier-linter-helpers "^1.0.0"
eslint-plugin-react-hooks@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.1.0.tgz#6323fbd5e650e84b2987ba76370523a60f4e7925"
integrity sha512-36zilUcDwDReiORXmcmTc6rRumu9JIM3WjSvV0nclHoUQ0CNrX866EwONvLR/UqaeqFutbAnVu8PEmctdo2SRQ==
eslint-plugin-react@^7.20.0: eslint-plugin-react@^7.20.0:
version "7.20.5" version "7.20.5"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.5.tgz#29480f3071f64a04b2c3d99d9b460ce0f76fb857" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.5.tgz#29480f3071f64a04b2c3d99d9b460ce0f76fb857"
@ -7877,18 +7882,17 @@ mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.3:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mobx-react-devtools@^6.0.3: mobx-react-lite@>=2.0.6:
version "6.1.1" version "2.0.7"
resolved "https://registry.yarnpkg.com/mobx-react-devtools/-/mobx-react-devtools-6.1.1.tgz#a462b944085cf11ff96fc937d12bf31dab4c8984" resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-2.0.7.tgz#1bfb3b4272668e288047cf0c7940b14e91cba284"
integrity sha512-nc5IXLdEUFLn3wZal65KF3/JFEFd+mbH4KTz/IG5BOPyw7jo8z29w/8qm7+wiCyqVfUIgJ1gL4+HVKmcXIOgqA== integrity sha512-YKAh2gThC6WooPnVZCoC+rV1bODAKFwkhxikzgH18wpBjkgTkkR9Sb0IesQAH5QrAEH/JQVmy47jcpQkf2Au3Q==
mobx-react@^5.4.2: mobx-react@^6.2.5:
version "5.4.4" version "6.2.5"
resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-5.4.4.tgz#b3de9c6eabcd0ed8a40036888cb0221ab9568b80" resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-6.2.5.tgz#9020a17b79cc6dc3d124ad89ab36eb9ea540a45b"
integrity sha512-2mTzpyEjVB/RGk2i6KbcmP4HWcAUFox5ZRCrGvSyz49w20I4C4qql63grPpYrS9E9GKwgydBHQlA4y665LuRCQ== integrity sha512-LxtXXW0GkOAO6VOIg2m/6WL6ZuKlzOWwESIFdrWelI0ZMIvtKCMZVUuulcO5GAWSDsH0ApaMkGLoaPqKjzyziQ==
dependencies: dependencies:
hoist-non-react-statics "^3.0.0" mobx-react-lite ">=2.0.6"
react-lifecycles-compat "^3.0.2"
mobx@4.6.0: mobx@4.6.0:
version "4.6.0" version "4.6.0"
@ -9346,7 +9350,7 @@ react-keydown@^1.7.3:
dependencies: dependencies:
core-js "^3.1.2" core-js "^3.1.2"
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.2: react-lifecycles-compat@^3.0.0:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==