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:
parent
179176c312
commit
ec38f5d79c
|
@ -4,7 +4,8 @@
|
|||
"react-app",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:flowtype/recommended"
|
||||
"plugin:flowtype/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"prettier",
|
||||
|
|
|
@ -14,7 +14,7 @@ export default function DelayedMount({ delay = 250, children }: Props) {
|
|||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, []);
|
||||
}, [delay]);
|
||||
|
||||
if (!isShowing) {
|
||||
return null;
|
||||
|
|
|
@ -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 <HoverPreviewInternal {...rest} node={node} />;
|
||||
}
|
||||
|
||||
const Animate = styled.div`
|
||||
animation: ${fadeAndSlideIn} 150ms ease;
|
||||
|
||||
|
|
|
@ -44,21 +44,22 @@ class Layout extends React.Component<Props> {
|
|||
@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+/")
|
||||
|
|
|
@ -17,7 +17,8 @@ class Mask extends React.Component<Props> {
|
|||
return false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
constructor() {
|
||||
super();
|
||||
this.width = randomInteger(75, 100);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Props> {
|
||||
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 = (
|
||||
<Container
|
||||
editMode={ui.editMode}
|
||||
const content = (
|
||||
<Container
|
||||
editMode={ui.editMode}
|
||||
mobileSidebarVisible={ui.mobileSidebarVisible}
|
||||
column
|
||||
>
|
||||
<Toggle
|
||||
onClick={ui.toggleMobileSidebar}
|
||||
mobileSidebarVisible={ui.mobileSidebarVisible}
|
||||
column
|
||||
>
|
||||
<Toggle
|
||||
onClick={this.toggleSidebar}
|
||||
mobileSidebarVisible={ui.mobileSidebarVisible}
|
||||
>
|
||||
{ui.mobileSidebarVisible ? (
|
||||
<CloseIcon size={32} />
|
||||
) : (
|
||||
<MenuIcon size={32} />
|
||||
)}
|
||||
</Toggle>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
{ui.mobileSidebarVisible ? (
|
||||
<CloseIcon size={32} />
|
||||
) : (
|
||||
<MenuIcon size={32} />
|
||||
)}
|
||||
</Toggle>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
|
||||
// Fade in the sidebar on first render after page load
|
||||
if (firstRender) {
|
||||
firstRender = false;
|
||||
return <Fade>{content}</Fade>;
|
||||
}
|
||||
|
||||
return content;
|
||||
// Fade in the sidebar on first render after page load
|
||||
if (firstRender) {
|
||||
firstRender = false;
|
||||
return <Fade>{content}</Fade>;
|
||||
}
|
||||
|
||||
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));
|
||||
|
|
|
@ -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<Props> {
|
||||
@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 (
|
||||
<Wrapper column>
|
||||
<StyledNavLink
|
||||
activeStyle={activeStyle}
|
||||
style={active ? activeStyle : this.style}
|
||||
onClick={onClick}
|
||||
exact={exact !== false}
|
||||
to={to}
|
||||
as={to ? undefined : href ? "a" : "div"}
|
||||
href={href}
|
||||
>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
<Label onClick={this.handleExpand}>
|
||||
{showDisclosure && (
|
||||
<Disclosure expanded={this.expanded} onClick={this.handleClick} />
|
||||
)}
|
||||
{label}
|
||||
</Label>
|
||||
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
|
||||
</StyledNavLink>
|
||||
{this.expanded && children}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<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
|
||||
|
@ -171,4 +171,4 @@ const Disclosure = styled(CollapsedIcon)`
|
|||
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
|
||||
`;
|
||||
|
||||
export default withRouter(withTheme(SidebarLink));
|
||||
export default withRouter(withTheme(observer(SidebarLink)));
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
36
app/index.js
36
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(
|
||||
<>
|
||||
<ErrorBoundary>
|
||||
<Provider {...stores}>
|
||||
<Theme>
|
||||
<Router>
|
||||
<>
|
||||
<ScrollToTop>
|
||||
<Routes />
|
||||
</ScrollToTop>
|
||||
<Toasts />
|
||||
</>
|
||||
</Router>
|
||||
</Theme>
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
{DevTools && <DevTools position={{ bottom: 0, right: 0 }} />}
|
||||
</>,
|
||||
<ErrorBoundary>
|
||||
<Provider {...stores}>
|
||||
<Theme>
|
||||
<Router>
|
||||
<>
|
||||
<ScrollToTop>
|
||||
<Routes />
|
||||
</ScrollToTop>
|
||||
<Toasts />
|
||||
</>
|
||||
</Router>
|
||||
</Theme>
|
||||
</Provider>
|
||||
</ErrorBoundary>,
|
||||
element
|
||||
);
|
||||
}
|
||||
|
|
|
@ -62,10 +62,10 @@ class CollectionScene extends React.Component<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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",
|
||||
|
|
26
yarn.lock
26
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==
|
||||
|
|
Reference in New Issue