Skip to content

Commit ec38f5d

Browse files
authored
refactor: Remove old react lifecycle methods (outline#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
1 parent 179176c commit ec38f5d

File tree

14 files changed

+192
-176
lines changed

14 files changed

+192
-176
lines changed

.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"react-app",
55
"plugin:import/errors",
66
"plugin:import/warnings",
7-
"plugin:flowtype/recommended"
7+
"plugin:flowtype/recommended",
8+
"plugin:react-hooks/recommended"
89
],
910
"plugins": [
1011
"prettier",

app/components/DelayedMount.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default function DelayedMount({ delay = 250, children }: Props) {
1414
return () => {
1515
clearTimeout(timeout);
1616
};
17-
}, []);
17+
}, [delay]);
1818

1919
if (!isShowing) {
2020
return null;

app/components/HoverPreview.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,7 @@ type Props = {
2020
onClose: () => void,
2121
};
2222

23-
function HoverPreview({ node, documents, onClose, event }: Props) {
24-
// previews only work for internal doc links for now
25-
if (!isInternalUrl(node.href)) {
26-
return null;
27-
}
28-
23+
function HoverPreviewInternal({ node, documents, onClose, event }: Props) {
2924
const slug = parseDocumentSlugFromUrl(node.href);
3025

3126
const [isVisible, setVisible] = React.useState(false);
@@ -131,6 +126,15 @@ function HoverPreview({ node, documents, onClose, event }: Props) {
131126
);
132127
}
133128

129+
function HoverPreview({ node, ...rest }: Props) {
130+
// previews only work for internal doc links for now
131+
if (!isInternalUrl(node.href)) {
132+
return null;
133+
}
134+
135+
return <HoverPreviewInternal {...rest} node={node} />;
136+
}
137+
134138
const Animate = styled.div`
135139
animation: ${fadeAndSlideIn} 150ms ease;
136140

app/components/Layout.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,22 @@ class Layout extends React.Component<Props> {
4444
@observable redirectTo: ?string;
4545
@observable keyboardShortcutsOpen: boolean = false;
4646

47-
componentWillMount() {
48-
this.updateBackground();
47+
constructor(props) {
48+
super();
49+
this.updateBackground(props);
4950
}
5051

5152
componentDidUpdate() {
52-
this.updateBackground();
53+
this.updateBackground(this.props);
5354

5455
if (this.redirectTo) {
5556
this.redirectTo = undefined;
5657
}
5758
}
5859

59-
updateBackground() {
60+
updateBackground(props) {
6061
// ensure the wider page color always matches the theme
61-
window.document.body.style.background = this.props.theme.background;
62+
window.document.body.style.background = props.theme.background;
6263
}
6364

6465
@keydown("shift+/")

app/components/Mask.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class Mask extends React.Component<Props> {
1717
return false;
1818
}
1919

20-
componentWillMount() {
20+
constructor() {
21+
super();
2122
this.width = randomInteger(75, 100);
2223
}
2324

app/components/Sidebar/Sidebar.js

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,60 @@
11
// @flow
2-
import { observer, inject } from "mobx-react";
2+
import { observer } from "mobx-react";
33
import { CloseIcon, MenuIcon } from "outline-icons";
44
import * as React from "react";
55
import { withRouter } from "react-router-dom";
66
import type { Location } from "react-router-dom";
77
import styled from "styled-components";
88
import breakpoint from "styled-components-breakpoint";
9-
import UiStore from "stores/UiStore";
109
import Fade from "components/Fade";
1110
import Flex from "components/Flex";
11+
import usePrevious from "hooks/usePrevious";
12+
import useStores from "hooks/useStores";
1213

1314
let firstRender = true;
1415

1516
type Props = {
1617
children: React.Node,
1718
location: Location,
18-
ui: UiStore,
1919
};
2020

21-
@observer
22-
class Sidebar extends React.Component<Props> {
23-
componentWillReceiveProps = (nextProps: Props) => {
24-
if (this.props.location !== nextProps.location) {
25-
this.props.ui.hideMobileSidebar();
26-
}
27-
};
21+
function Sidebar({ location, children }: Props) {
22+
const { ui } = useStores();
23+
const previousLocation = usePrevious(location);
2824

29-
toggleSidebar = () => {
30-
this.props.ui.toggleMobileSidebar();
31-
};
25+
React.useEffect(() => {
26+
if (location !== previousLocation) {
27+
ui.hideMobileSidebar();
28+
}
29+
}, [ui, location]);
3230

33-
render() {
34-
const { children, ui } = this.props;
35-
const content = (
36-
<Container
37-
editMode={ui.editMode}
31+
const content = (
32+
<Container
33+
editMode={ui.editMode}
34+
mobileSidebarVisible={ui.mobileSidebarVisible}
35+
column
36+
>
37+
<Toggle
38+
onClick={ui.toggleMobileSidebar}
3839
mobileSidebarVisible={ui.mobileSidebarVisible}
39-
column
4040
>
41-
<Toggle
42-
onClick={this.toggleSidebar}
43-
mobileSidebarVisible={ui.mobileSidebarVisible}
44-
>
45-
{ui.mobileSidebarVisible ? (
46-
<CloseIcon size={32} />
47-
) : (
48-
<MenuIcon size={32} />
49-
)}
50-
</Toggle>
51-
{children}
52-
</Container>
53-
);
41+
{ui.mobileSidebarVisible ? (
42+
<CloseIcon size={32} />
43+
) : (
44+
<MenuIcon size={32} />
45+
)}
46+
</Toggle>
47+
{children}
48+
</Container>
49+
);
5450

55-
// Fade in the sidebar on first render after page load
56-
if (firstRender) {
57-
firstRender = false;
58-
return <Fade>{content}</Fade>;
59-
}
60-
61-
return content;
51+
// Fade in the sidebar on first render after page load
52+
if (firstRender) {
53+
firstRender = false;
54+
return <Fade>{content}</Fade>;
6255
}
56+
57+
return content;
6358
}
6459

6560
const Container = styled(Flex)`
@@ -117,4 +112,4 @@ const Toggle = styled.a`
117112
`};
118113
`;
119114

120-
export default withRouter(inject("ui")(Sidebar));
115+
export default withRouter(observer(Sidebar));

app/components/Sidebar/components/SidebarLink.js

Lines changed: 71 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// @flow
2-
import { observable, action } from "mobx";
32
import { observer } from "mobx-react";
43
import { CollapsedIcon } from "outline-icons";
54
import * as React from "react";
@@ -25,79 +24,80 @@ type Props = {
2524
depth?: number,
2625
};
2726

28-
@observer
29-
class SidebarLink extends React.Component<Props> {
30-
@observable expanded: ?boolean = this.props.expanded;
31-
32-
style = {
33-
paddingLeft: `${(this.props.depth || 0) * 16 + 16}px`,
34-
};
27+
function SidebarLink({
28+
icon,
29+
children,
30+
onClick,
31+
to,
32+
label,
33+
active,
34+
menu,
35+
menuOpen,
36+
hideDisclosure,
37+
theme,
38+
exact,
39+
href,
40+
depth,
41+
...rest
42+
}: Props) {
43+
const [expanded, setExpanded] = React.useState(rest.expanded);
44+
45+
const style = React.useMemo(() => {
46+
return {
47+
paddingLeft: `${(depth || 0) * 16 + 16}px`,
48+
};
49+
}, [depth]);
3550

36-
componentWillReceiveProps(nextProps: Props) {
37-
if (nextProps.expanded !== undefined) {
38-
this.expanded = nextProps.expanded;
51+
React.useEffect(() => {
52+
if (rest.expanded) {
53+
setExpanded(rest.expanded);
3954
}
40-
}
41-
42-
@action
43-
handleClick = (ev: SyntheticEvent<>) => {
44-
ev.preventDefault();
45-
ev.stopPropagation();
46-
47-
this.expanded = !this.expanded;
55+
}, [rest.expanded]);
56+
57+
const handleClick = React.useCallback(
58+
(ev: SyntheticEvent<>) => {
59+
ev.preventDefault();
60+
ev.stopPropagation();
61+
setExpanded(!expanded);
62+
},
63+
[expanded]
64+
);
65+
66+
const handleExpand = React.useCallback(() => {
67+
setExpanded(true);
68+
}, []);
69+
70+
const showDisclosure = !!children && !hideDisclosure;
71+
const activeStyle = {
72+
color: theme.text,
73+
background: theme.sidebarItemBackground,
74+
fontWeight: 600,
75+
...style,
4876
};
4977

50-
@action
51-
handleExpand = () => {
52-
this.expanded = true;
53-
};
54-
55-
render() {
56-
const {
57-
icon,
58-
children,
59-
onClick,
60-
to,
61-
label,
62-
active,
63-
menu,
64-
menuOpen,
65-
hideDisclosure,
66-
exact,
67-
href,
68-
} = this.props;
69-
const showDisclosure = !!children && !hideDisclosure;
70-
const activeStyle = {
71-
color: this.props.theme.text,
72-
background: this.props.theme.sidebarItemBackground,
73-
fontWeight: 600,
74-
...this.style,
75-
};
76-
77-
return (
78-
<Wrapper column>
79-
<StyledNavLink
80-
activeStyle={activeStyle}
81-
style={active ? activeStyle : this.style}
82-
onClick={onClick}
83-
exact={exact !== false}
84-
to={to}
85-
as={to ? undefined : href ? "a" : "div"}
86-
href={href}
87-
>
88-
{icon && <IconWrapper>{icon}</IconWrapper>}
89-
<Label onClick={this.handleExpand}>
90-
{showDisclosure && (
91-
<Disclosure expanded={this.expanded} onClick={this.handleClick} />
92-
)}
93-
{label}
94-
</Label>
95-
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
96-
</StyledNavLink>
97-
{this.expanded && children}
98-
</Wrapper>
99-
);
100-
}
78+
return (
79+
<Wrapper column>
80+
<StyledNavLink
81+
activeStyle={activeStyle}
82+
style={active ? activeStyle : style}
83+
onClick={onClick}
84+
exact={exact !== false}
85+
to={to}
86+
as={to ? undefined : href ? "a" : "div"}
87+
href={href}
88+
>
89+
{icon && <IconWrapper>{icon}</IconWrapper>}
90+
<Label onClick={handleExpand}>
91+
{showDisclosure && (
92+
<Disclosure expanded={expanded} onClick={handleClick} />
93+
)}
94+
{label}
95+
</Label>
96+
{menu && <Action menuOpen={menuOpen}>{menu}</Action>}
97+
</StyledNavLink>
98+
{expanded && children}
99+
</Wrapper>
100+
);
101101
}
102102

103103
// accounts for whitespace around icon
@@ -171,4 +171,4 @@ const Disclosure = styled(CollapsedIcon)`
171171
${({ expanded }) => !expanded && "transform: rotate(-90deg);"};
172172
`;
173173

174-
export default withRouter(withTheme(SidebarLink));
174+
export default withRouter(withTheme(observer(SidebarLink)));

app/hooks/usePrevious.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// @flow
2+
import * as React from "react";
3+
4+
export default function usePrevious(value: any) {
5+
const ref = React.useRef();
6+
React.useEffect(() => {
7+
ref.current = value;
8+
});
9+
return ref.current;
10+
}

app/hooks/useStores.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @flow
2+
import { MobXProviderContext } from "mobx-react";
3+
import * as React from "react";
4+
import RootStore from "stores";
5+
6+
export default function useStores(): typeof RootStore {
7+
return React.useContext(MobXProviderContext);
8+
}

0 commit comments

Comments
 (0)