From ec38f5d79c16c61b318cae35799442d3a48a052c Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 23 Aug 2020 11:51:56 -0700 Subject: [PATCH] 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 --- .eslintrc | 3 +- app/components/DelayedMount.js | 2 +- app/components/HoverPreview.js | 16 +- app/components/Layout.js | 11 +- app/components/Mask.js | 3 +- app/components/Sidebar/Sidebar.js | 75 +++++---- .../Sidebar/components/SidebarLink.js | 144 +++++++++--------- app/hooks/usePrevious.js | 10 ++ app/hooks/useStores.js | 8 + app/index.js | 36 ++--- app/scenes/Collection.js | 6 +- app/stores/UiStore.js | 24 +-- package.json | 6 +- yarn.lock | 26 ++-- 14 files changed, 193 insertions(+), 177 deletions(-) create mode 100644 app/hooks/usePrevious.js create mode 100644 app/hooks/useStores.js diff --git a/.eslintrc b/.eslintrc index 87aabc37..8eeaa95b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,7 +4,8 @@ "react-app", "plugin:import/errors", "plugin:import/warnings", - "plugin:flowtype/recommended" + "plugin:flowtype/recommended", + "plugin:react-hooks/recommended" ], "plugins": [ "prettier", diff --git a/app/components/DelayedMount.js b/app/components/DelayedMount.js index 65c51578..e127456c 100644 --- a/app/components/DelayedMount.js +++ b/app/components/DelayedMount.js @@ -14,7 +14,7 @@ export default function DelayedMount({ delay = 250, children }: Props) { return () => { clearTimeout(timeout); }; - }, []); + }, [delay]); if (!isShowing) { return null; diff --git a/app/components/HoverPreview.js b/app/components/HoverPreview.js index 1b05cf01..198f5eeb 100644 --- a/app/components/HoverPreview.js +++ b/app/components/HoverPreview.js @@ -20,12 +20,7 @@ type Props = { onClose: () => void, }; -function HoverPreview({ node, documents, onClose, event }: Props) { - // previews only work for internal doc links for now - if (!isInternalUrl(node.href)) { - return null; - } - +function HoverPreviewInternal({ node, documents, onClose, event }: Props) { const slug = parseDocumentSlugFromUrl(node.href); 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 ; +} + const Animate = styled.div` animation: ${fadeAndSlideIn} 150ms ease; diff --git a/app/components/Layout.js b/app/components/Layout.js index 882f3f9d..fcd68518 100644 --- a/app/components/Layout.js +++ b/app/components/Layout.js @@ -44,21 +44,22 @@ class Layout extends React.Component { @observable redirectTo: ?string; @observable keyboardShortcutsOpen: boolean = false; - componentWillMount() { - this.updateBackground(); + constructor(props) { + super(); + this.updateBackground(props); } componentDidUpdate() { - this.updateBackground(); + this.updateBackground(this.props); if (this.redirectTo) { this.redirectTo = undefined; } } - updateBackground() { + updateBackground(props) { // 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+/") diff --git a/app/components/Mask.js b/app/components/Mask.js index 66440f42..d581ea3b 100644 --- a/app/components/Mask.js +++ b/app/components/Mask.js @@ -17,7 +17,8 @@ class Mask extends React.Component { return false; } - componentWillMount() { + constructor() { + super(); this.width = randomInteger(75, 100); } diff --git a/app/components/Sidebar/Sidebar.js b/app/components/Sidebar/Sidebar.js index c26c7dcd..6941c5ca 100644 --- a/app/components/Sidebar/Sidebar.js +++ b/app/components/Sidebar/Sidebar.js @@ -1,65 +1,60 @@ // @flow -import { observer, inject } from "mobx-react"; +import { observer } from "mobx-react"; import { CloseIcon, MenuIcon } from "outline-icons"; import * as React from "react"; import { withRouter } from "react-router-dom"; import type { Location } from "react-router-dom"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; -import UiStore from "stores/UiStore"; import Fade from "components/Fade"; import Flex from "components/Flex"; +import usePrevious from "hooks/usePrevious"; +import useStores from "hooks/useStores"; let firstRender = true; type Props = { children: React.Node, location: Location, - ui: UiStore, }; -@observer -class Sidebar extends React.Component { - componentWillReceiveProps = (nextProps: Props) => { - if (this.props.location !== nextProps.location) { - this.props.ui.hideMobileSidebar(); +function Sidebar({ location, children }: Props) { + const { ui } = useStores(); + const previousLocation = usePrevious(location); + + React.useEffect(() => { + if (location !== previousLocation) { + ui.hideMobileSidebar(); } - }; + }, [ui, location]); - toggleSidebar = () => { - this.props.ui.toggleMobileSidebar(); - }; - - render() { - const { children, ui } = this.props; - const content = ( - + - - {ui.mobileSidebarVisible ? ( - - ) : ( - - )} - - {children} - - ); + {ui.mobileSidebarVisible ? ( + + ) : ( + + )} + + {children} + + ); - // Fade in the sidebar on first render after page load - if (firstRender) { - firstRender = false; - return {content}; - } - - return content; + // Fade in the sidebar on first render after page load + if (firstRender) { + firstRender = false; + return {content}; } + + return content; } const Container = styled(Flex)` @@ -117,4 +112,4 @@ const Toggle = styled.a` `}; `; -export default withRouter(inject("ui")(Sidebar)); +export default withRouter(observer(Sidebar)); diff --git a/app/components/Sidebar/components/SidebarLink.js b/app/components/Sidebar/components/SidebarLink.js index 2d08f572..f47bec3e 100644 --- a/app/components/Sidebar/components/SidebarLink.js +++ b/app/components/Sidebar/components/SidebarLink.js @@ -1,5 +1,4 @@ // @flow -import { observable, action } from "mobx"; import { observer } from "mobx-react"; import { CollapsedIcon } from "outline-icons"; import * as React from "react"; @@ -25,79 +24,80 @@ type Props = { depth?: number, }; -@observer -class SidebarLink extends React.Component { - @observable expanded: ?boolean = this.props.expanded; +function SidebarLink({ + icon, + children, + onClick, + to, + label, + active, + menu, + menuOpen, + hideDisclosure, + theme, + exact, + href, + depth, + ...rest +}: Props) { + const [expanded, setExpanded] = React.useState(rest.expanded); - style = { - paddingLeft: `${(this.props.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, + const style = React.useMemo(() => { + return { + paddingLeft: `${(depth || 0) * 16 + 16}px`, }; + }, [depth]); - return ( - - - {icon && {icon}} - - {menu && {menu}} - - {this.expanded && children} - - ); - } + React.useEffect(() => { + if (rest.expanded) { + setExpanded(rest.expanded); + } + }, [rest.expanded]); + + const handleClick = React.useCallback( + (ev: SyntheticEvent<>) => { + ev.preventDefault(); + ev.stopPropagation(); + setExpanded(!expanded); + }, + [expanded] + ); + + const handleExpand = React.useCallback(() => { + setExpanded(true); + }, []); + + const showDisclosure = !!children && !hideDisclosure; + const activeStyle = { + color: theme.text, + background: theme.sidebarItemBackground, + fontWeight: 600, + ...style, + }; + + return ( + + + {icon && {icon}} + + {menu && {menu}} + + {expanded && children} + + ); } // accounts for whitespace around icon @@ -171,4 +171,4 @@ const Disclosure = styled(CollapsedIcon)` ${({ expanded }) => !expanded && "transform: rotate(-90deg);"}; `; -export default withRouter(withTheme(SidebarLink)); +export default withRouter(withTheme(observer(SidebarLink))); diff --git a/app/hooks/usePrevious.js b/app/hooks/usePrevious.js new file mode 100644 index 00000000..501ef32e --- /dev/null +++ b/app/hooks/usePrevious.js @@ -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; +} diff --git a/app/hooks/useStores.js b/app/hooks/useStores.js new file mode 100644 index 00000000..f7873aca --- /dev/null +++ b/app/hooks/useStores.js @@ -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); +} diff --git a/app/index.js b/app/index.js index 8c2d3ad1..41b75aa4 100644 --- a/app/index.js +++ b/app/index.js @@ -12,32 +12,24 @@ import Toasts from "components/Toasts"; import Routes from "./routes"; 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"); if (element) { render( - <> - - - - - <> - - - - - - - - - - {DevTools && } - , + + + + + <> + + + + + + + + + , element ); } diff --git a/app/scenes/Collection.js b/app/scenes/Collection.js index 9a4e9b99..3073d5ae 100644 --- a/app/scenes/Collection.js +++ b/app/scenes/Collection.js @@ -62,10 +62,10 @@ class CollectionScene extends React.Component { } } - componentWillReceiveProps(nextProps) { - const { id } = nextProps.match.params; + componentDidUpdate(prevProps) { + const { id } = this.props.match.params; - if (id && id !== this.props.match.params.id) { + if (id && id !== prevProps.match.params.id) { this.loadContent(id); } } diff --git a/app/stores/UiStore.js b/app/stores/UiStore.js index 676dbb99..e1996464 100644 --- a/app/stores/UiStore.js +++ b/app/stores/UiStore.js @@ -109,34 +109,34 @@ class UiStore { }; @action - enableEditMode() { + enableEditMode = () => { this.editMode = true; - } + }; @action - disableEditMode() { + disableEditMode = () => { this.editMode = false; - } + }; @action - enableProgressBar() { + enableProgressBar = () => { this.progressBarVisible = true; - } + }; @action - disableProgressBar() { + disableProgressBar = () => { this.progressBarVisible = false; - } + }; @action - toggleMobileSidebar() { + toggleMobileSidebar = () => { this.mobileSidebarVisible = !this.mobileSidebarVisible; - } + }; @action - hideMobileSidebar() { + hideMobileSidebar = () => { this.mobileSidebarVisible = false; - } + }; @action showToast = ( diff --git a/package.json b/package.json index 39d7be4f..ac108c51 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "koa-static": "^4.0.1", "lodash": "^4.17.19", "mobx": "4.6.0", - "mobx-react": "^5.4.2", + "mobx-react": "^6.2.5", "natural-sort": "^1.0.0", "nodemailer": "^4.4.0", "outline-icons": "^1.21.0-6", @@ -170,13 +170,13 @@ "eslint-plugin-jsx-a11y": "^6.1.0", "eslint-plugin-prettier": "^3.1.0", "eslint-plugin-react": "^7.20.0", + "eslint-plugin-react-hooks": "^4.1.0", "fetch-test-server": "^1.1.0", "flow-bin": "^0.104.0", "html-webpack-plugin": "3.2.0", "jest-cli": "^26.0.0", "koa-webpack-dev-middleware": "^1.4.5", "koa-webpack-hot-middleware": "^1.0.3", - "mobx-react-devtools": "^6.0.3", "nodemon": "^1.19.4", "prettier": "^2.0.5", "rimraf": "^2.5.4", @@ -191,4 +191,4 @@ "js-yaml": "^3.13.1" }, "version": "0.46.0" -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 79c095a8..1647b6ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4383,6 +4383,11 @@ eslint-plugin-prettier@^3.1.0: dependencies: 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: version "7.20.5" 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" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mobx-react-devtools@^6.0.3: - version "6.1.1" - resolved "https://registry.yarnpkg.com/mobx-react-devtools/-/mobx-react-devtools-6.1.1.tgz#a462b944085cf11ff96fc937d12bf31dab4c8984" - integrity sha512-nc5IXLdEUFLn3wZal65KF3/JFEFd+mbH4KTz/IG5BOPyw7jo8z29w/8qm7+wiCyqVfUIgJ1gL4+HVKmcXIOgqA== +mobx-react-lite@>=2.0.6: + version "2.0.7" + resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-2.0.7.tgz#1bfb3b4272668e288047cf0c7940b14e91cba284" + integrity sha512-YKAh2gThC6WooPnVZCoC+rV1bODAKFwkhxikzgH18wpBjkgTkkR9Sb0IesQAH5QrAEH/JQVmy47jcpQkf2Au3Q== -mobx-react@^5.4.2: - version "5.4.4" - resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-5.4.4.tgz#b3de9c6eabcd0ed8a40036888cb0221ab9568b80" - integrity sha512-2mTzpyEjVB/RGk2i6KbcmP4HWcAUFox5ZRCrGvSyz49w20I4C4qql63grPpYrS9E9GKwgydBHQlA4y665LuRCQ== +mobx-react@^6.2.5: + version "6.2.5" + resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-6.2.5.tgz#9020a17b79cc6dc3d124ad89ab36eb9ea540a45b" + integrity sha512-LxtXXW0GkOAO6VOIg2m/6WL6ZuKlzOWwESIFdrWelI0ZMIvtKCMZVUuulcO5GAWSDsH0ApaMkGLoaPqKjzyziQ== dependencies: - hoist-non-react-statics "^3.0.0" - react-lifecycles-compat "^3.0.2" + mobx-react-lite ">=2.0.6" mobx@4.6.0: version "4.6.0" @@ -9346,7 +9350,7 @@ react-keydown@^1.7.3: dependencies: 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" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==