diff --git a/.eslintrc b/.eslintrc
index 87aabc376b64..8eeaa95b1dce 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 65c515784375..e127456c98d8 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 1b05cf01c933..198f5eeba910 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 882f3f9de31e..fcd685189576 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 66440f42339f..d581ea3bdfdb 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 c26c7dcdc00e..6941c5ca3eb9 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);
- toggleSidebar = () => {
- this.props.ui.toggleMobileSidebar();
- };
+ React.useEffect(() => {
+ if (location !== previousLocation) {
+ ui.hideMobileSidebar();
+ }
+ }, [ui, location]);
- 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 2d08f572600c..f47bec3e20a8 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;
-
- style = {
- paddingLeft: `${(this.props.depth || 0) * 16 + 16}px`,
- };
+function SidebarLink({
+ icon,
+ children,
+ onClick,
+ to,
+ label,
+ active,
+ menu,
+ menuOpen,
+ hideDisclosure,
+ theme,
+ exact,
+ href,
+ depth,
+ ...rest
+}: Props) {
+ const [expanded, setExpanded] = React.useState(rest.expanded);
+
+ const style = React.useMemo(() => {
+ return {
+ paddingLeft: `${(depth || 0) * 16 + 16}px`,
+ };
+ }, [depth]);
- componentWillReceiveProps(nextProps: Props) {
- if (nextProps.expanded !== undefined) {
- this.expanded = nextProps.expanded;
+ React.useEffect(() => {
+ if (rest.expanded) {
+ setExpanded(rest.expanded);
}
- }
-
- @action
- handleClick = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
- ev.stopPropagation();
-
- this.expanded = !this.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,
};
- @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,
- };
-
- return (
-
-
- {icon && {icon}}
-
- {menu && {menu}}
-
- {this.expanded && children}
-
- );
- }
+ 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 000000000000..501ef32ef730
--- /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 000000000000..f7873aca6263
--- /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 8c2d3ad1fb2e..41b75aa4b7c8 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 9a4e9b99b53f..3073d5aed89d 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 676dbb99a1dd..e1996464852e 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 39d7be4fc80f..ac108c514603 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 79c095a831d4..1647b6ef378c 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==