This pattern converts React class components to functional components, with hooks.
Apply with the Grit CLI
grit apply react_to_hooks
Converts a React class component
BEFORE
import { Component } from 'react'; class App extends Component { constructor(...args) { super(args); this.state = { name: '', another: 3, }; } static foo = 1; static fooBar = 21; static bar = (input) => { console.log(input); }; static another(input) { console.error(input); } componentDidMount() { document.title = `You clicked ${this.state.count} times`; } componentDidUpdate(prevProps) { // alert("This component was mounted"); document.title = `You clicked ${this.state.count} times`; const { isOpen } = this.state; if (isOpen && !prevProps.isOpen) { alert('You just opened the modal!'); } } alertName = () => { alert(this.state.name); }; handleNameInput = (e) => { this.setState({ name: e.target.value, another: 'cooler' }); }; async asyncAlert() { await alert('async alert'); } render() { return ( <div> <h3>This is a Class Component</h3> <input type='text' onChange={this.handleNameInput} value={this.state.name} placeholder='Your Name' /> <button onClick={this.alertName}>Alert</button> <button onClick={this.asyncAlert}>Alert</button> </div> ); } }
AFTER
import { useState, useEffect, useCallback, useRef } from 'react'; const App = (props) => { const [name, setName] = useState(''); const [another, setAnother] = useState(3); const [count, setCount] = useState(); const [isOpen, setIsOpen] = useState(); const prevProps = usePreviousValue(props); useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); useEffect(() => { // alert("This component was mounted"); document.title = `You clicked ${count} times`; if (isOpen && !prevProps.isOpen) { alert('You just opened the modal!'); } }, [count, isOpen, prevProps]); const alertNameHandler = useCallback(() => { alert(name); }, [name]); const handleNameInputHandler = useCallback((e) => { setName(e.target.value); setAnother('cooler'); }, []); const asyncAlertHandler = useCallback(async () => { await alert('async alert'); }, []); return ( <div> <h3>This is a Class Component</h3> <input type='text' onChange={handleNameInputHandler} value={name} placeholder='Your Name' /> <button onClick={alertNameHandler}>Alert</button> <button onClick={asyncAlertHandler}>Alert</button> </div> ); }; App.foo = 1; App.fooBar = 21; App.bar = (input) => { console.log(input); }; App.another = (input) => { console.error(input); }; function usePreviousValue() { const ref = useRef(); useEffect(() => { ref.current = props; }); return ref.current; }
MobX - Observables and Computed
BEFORE
import React from 'react'; class SampleComponent extends React.Component { onClick = () => { this.clicks = this.clicks + 1; }; @observable private clicks = this.props.initialCount; @computed private get isEven() { return this.clicks % 2 === 0; } render() { return ( <> <p>Clicks: {this.clicks}</p> <p>Is even: {this.isEven}</p> <a onClick={this.onClick}>click</a> </> ); } }
AFTER
import React, { useState, useCallback } from 'react'; const SampleComponent = (props) => { const [clicks, setClicks] = useState(props.initialCount); const onClickHandler = useCallback(() => { setClicks(clicks + 1); }, [clicks]); const isEven = useMemo(() => { return clicks % 2 === 0; }, [clicks]); return ( <> <p>Clicks: {clicks}</p> <p>Is even: {isEven}</p> <a onClick={onClickHandler}>click</a> </> ); };
MobX - reactions
BEFORE
import React from 'react'; class SampleComponent extends React.Component { onClick = () => { this.clicks = this.clicks + 1; }; @observable private clicks = this.props.initialCount; @disposeOnUnmount disposer = reaction( () => this.clicks, (clicks) => { console.log("clicks", clicks); } ); @disposeOnUnmount propReaction = reaction( () => this.props.initialValue, () => { console.log("second click handler"); } ); render() { return ( <> <p>Clicks: {this.clicks}</p> <a onClick={this.onClick}>click</a> </> ); } }
AFTER
import React, { useState, useCallback, useEffect } from 'react'; const SampleComponent = (props) => { const [clicks, setClicks] = useState(props.initialCount); const onClickHandler = useCallback(() => { setClicks(clicks + 1); }, [clicks]); useEffect(() => { console.log('clicks', clicks); }, [clicks]); useEffect(() => { console.log('second click handler'); }, []); return ( <> <p>Clicks: {clicks}</p> <a onClick={onClickHandler}>click</a> </> ); };
Only processes top-level components
JS
import React from 'react'; class FooClass { static component = class extends React.Component { render() { return <div>Foo</div>; } }; }
MobX - ViewState
BEFORE
import { Component } from 'react'; class SampleComponent extends Component { private viewState = new ViewState(); render() { return ( <p>This component has a <span onClick={this.viewState.click}>ViewState</span></p> ); } }
AFTER
import { observer } from 'mobx-react'; import { useRef } from 'react'; const SampleComponentBase = () => { const viewState = useRef(new ViewState()); return ( <p> This component has a <span onClick={viewState.current.click}>ViewState</span> </p> ); }; export const SampleComponent = observer(SampleComponentBase);
Prop types are preserved
BEFORE
import React from 'react'; interface Props { name: string; } class SampleComponent extends React.Component<Props> { render() { return ( <> <p>Hello {this.props.name}</p> </> ); } }
AFTER
import React from 'react'; interface Props { name: string; } const SampleComponent = (props: Props) => { return ( <> <p>Hello {props.name}</p> </> ); };
Handle lifecycle events
BEFORE
import { Component } from 'react'; import PropTypes from 'prop-types'; class Foo extends Component { componentDidMount() { console.log('mounted'); } componentWillUnmount() { console.log('unmounted'); } render() { return <p>Foo</p>; } } export default Foo;
AFTER
import { useEffect } from 'react'; import PropTypes from 'prop-types'; const Foo = () => { useEffect(() => { console.log('mounted'); }, []); useEffect(() => { return () => { console.log('unmounted'); }; }, []); return <p>Foo</p>; }; export default Foo;
Pure JavaScript works, with no types inserted
BEFORE
import { Component } from 'react'; import PropTypes from 'prop-types'; class Link extends Component { static propTypes = { href: PropTypes.string.isRequired, }; render() { const { href } = this.props; return <a href={href}>Link Text</a>; } } export default Link;
AFTER
import { Component } from 'react'; import PropTypes from 'prop-types'; const Link = (props) => { const { href } = props; return <a href={href}>Link Text</a>; }; Link.propTypes = { href: PropTypes.string.isRequired, }; export default Link;
Null observables work
BEFORE
import React from "react"; class ObservedComponent extends React.Component { @observable private name?: string; @observable private age = 21; render() { const { name } = this; return ( <> <p>Hello {name}, you are {this.age}</p> </> ); } }
AFTER
import React, { useState } from 'react'; const ObservedComponent = () => { const [name, setName] = useState<string>(undefined); const [age, setAge] = useState(21); return ( <> <p> Hello {name}, you are {age} </p> </> ); };
MobX types are preserved and default props are good
BEFORE
import React from "react"; interface Person { name: string; } class ObservedComponent extends React.Component { static defaultProps = { king: "viking" }; @observable private me: Person = { name: "John", }; render() { return ( <> <p>This is {this.me.name}, {this.props.king}</p> </> ); } }
AFTER
import React, { useState } from 'react'; interface Person { name: string; } const ObservedComponent = (inputProps) => { const [me, setMe] = useState<Person>({ name: 'John', }); const props = { king: 'viking', ...inputProps, }; return ( <> <p> This is {me.name}, {props.king} </p> </> ); };
Use function component type definitions
If the codebase already uses FunctionComponent
, use it.
BEFORE
import { Component } from 'react'; const OtherComponent: React.FunctionComponent<{}> = () => { return <>Other</>; }; class Link extends Component { state = { visible: false, }; render() { return <></>; } } export default Link;
AFTER
import { useState } from 'react'; const OtherComponent: React.FunctionComponent<{}> = () => { return <>Other</>; }; const Link: React.FunctionComponent<{}> = () => { const [visible, setVisible] = useState(false); return <></>; }; export default Link;
State defined as class attribute
BEFORE
import { Component } from 'react'; class Link extends Component { state = { visible: false, }; render() { return <></>; } } export default Link;
AFTER
import { useState } from 'react'; const Link = () => { const [visible, setVisible] = useState(false); return <></>; }; export default Link;
Identifier conflicts
Notice how the showDetails in show()
should not be replaced.
BEFORE
import React, { Component, ReactNode } from 'react' class InnerStuff extends Component<Props, State> { override state: State = { visible: false, showDetails: true } constructor(props: Props) { super(props) } render() { return <>Component</> } show(options: Options): void { const { otherStuff, showDetails = true, } = options; console.log("options are", showDetails); } }
AFTER
import React, { useState, useCallback, ReactNode } from 'react'; const InnerStuff = () => { const [visible, setVisible] = useState(false); const [showDetails, setShowDetails] = useState(true); const showHandler = useCallback( (options: Options) => { const { otherStuff, showDetails = true } = options; console.log('options are', showDetails); }, [showDetails], ); return <>Component</>; };
State defined in interface
BEFORE
import { Component } from 'react'; class Link extends Component<Props, State> { render() { return <></>; } } interface State { visible?: boolean; } export default Link;
AFTER
import { useState } from 'react'; const Link = () => { const [visible, setVisible] = useState<boolean | undefined>(undefined); return <></>; }; interface State { visible?: boolean; } export default Link;
Preserves constructor logic
BEFORE
import { Component } from 'react'; class MyComponent extends Component { constructor(props: Props) { super(props); const five = 2 + 3; this.doSomething = this.doSomething.bind(this) this.saySomething = this.saySomething.bind(this) this.saySomething(); if(five === 5){ console.log("Hello"); this.doSomething(); } this.state = { secret: five, }; } saySomething() { console.log('hi'); } doSomething(){ console.log("Welcome") } render() { return <></>; } }
AFTER
import { useState, useCallback } from 'react'; const MyComponent = () => { const [secret, setSecret] = useState(five); const saySomethingHandler = useCallback(() => { console.log('hi'); }, []); const doSomethingHandler = useCallback(() => { console.log("Welcome"); }, []); const five = 2 + 3; saySomethingHandler(); if (five === 5) { console.log("Hello") doSomethingHandler() } return <></>; };
Initializes and sets refs correctly
BEFORE
import { Component } from 'react'; class Link extends Component { input = React.createRef<string>() private previouslyFocusedTextInput: InputHandle = {} show(options: Options): void { this.input.current = 'Hello world' this.previouslyFocusedTextInput = KeyboardHelper.currentlyFocusedInput() } render() { return <></>; } } export default Link;
AFTER
import { useRef, useCallback } from 'react'; const Link = () => { const input = useRef<string>(); const previouslyFocusedTextInput = useRef<InputHandle>({}); const showHandler = useCallback((options: Options) => { input.current = 'Hello world'; previouslyFocusedTextInput.current = KeyboardHelper.currentlyFocusedInput(); }, []); return <></>; }; export default Link;
Preserves comments
BEFORE
class MyComponent extends Component<PropsWithChildren> { /** * Comment on a static variable */ private static someVariable: number | undefined /** * Comment on a private class property */ private lucy = 'good' render() { return <></> } }
AFTER
import { useRef } from 'react'; const MyComponent = () => { /** * Comment on a private class property */ const lucy = useRef('good'); return <></>; }; /** * Comment on a static variable */ MyComponent.someVariable = undefined;
Handles an inline export
BEFORE
import { Component } from 'react'; export class MyComponent extends Component { constructor(props: Props) { this.state = { secret: 5, }; } render() { return <></>; } }
AFTER
import { useState } from 'react'; export const MyComponent = () => { const [secret, setSecret] = useState(5); return <></>; };
Handles an inline default export
BEFORE
import { Component } from 'react'; export default class MyComponent extends Component { constructor(props: Props) { this.state = { secret: 5, }; } render() { return <></>; } }
AFTER
import { useState } from 'react'; const MyComponent = () => { const [secret, setSecret] = useState(5); return <></>; }; export default MyComponent;
Does not remove existing react imports
BEFORE
import React, { ReactNode } from 'react'; export type Props = { children: ReactNode, }; type State = { open: boolean, }; export default class Expandable extends React.Component<Props, State> { constructor(props: Props) { super(props); this.state = { open: false }; } handleToggle() { this.setState(({ open }) => ({ open: !open })); } render() { return null; } }
AFTER
import React, { ReactNode, useState, useCallback } from 'react'; export type Props = { children: ReactNode; }; type State = { open: boolean; }; const Expandable = () => { const [open, setOpen] = useState(false); const handleToggleHandler = useCallback(() => { setOpen((open) => !open); }, [open]); return null; }; export default Expandable;
Identifies state which is accessed but not initialized
BEFORE
import React, { ReactNode } from 'react'; export type Props = { children: ReactNode, }; type State = { error: Error, show: boolean, }; export default class Expandable extends React.Component<Props, State> { componentDidUpdate(prevProps: Props) { if (this.state.error) { console.error(this.state.error); } } handleVerify() { sendRequest().catch((error) => this.setState({ error })); } render() { const { show } = this.state; return show ? <></> : null; } }
AFTER
import React, { ReactNode, useState, useEffect, useCallback } from 'react'; export type Props = { children: ReactNode; }; type State = { error: Error; show: boolean; }; const Expandable = () => { const [error, setError] = useState(); const [show, setShow] = useState(); useEffect(() => { if (error) { console.error(error); } }, [error]); const handleVerifyHandler = useCallback(() => { sendRequest().catch((error) => { setError(error); }); }, [error]); return show ? <></> : null; }; export default Expandable;
Handles implicit return
BEFORE
import React from 'react'; export default class Expandable extends React.Component { constructor(value) { this.state = { value, error: undefined }; } handleVerify() { sendRequest() .then((res) => this.setState({ value: res, error: undefined })) .catch((error) => this.setState({ error })); } render() { return null; } }
AFTER
import React, { useState, useCallback } from 'react'; const Expandable = () => { const [error, setError] = useState(undefined); const handleVerifyHandler = useCallback(() => { sendRequest() .then((res) => { setValue(res); setError(undefined); }) .catch((error) => { setError(error); }); }, [error]); return null; }; export default Expandable;
Sets state correctly where the state is an object
BEFORE
import React from 'react'; export default class Expandable extends React.PureComponent { componentDidMount() { this.setState({ dashboard: { label: result.dashboard_title, value: result.id }, }); } handleClick() { console.log(this.state.dashboard); } render() { return null; } }
AFTER
import React, { useState, useEffect, useCallback } from 'react'; const Expandable = () => { const [dashboard, setDashboard] = useState(); useEffect(() => { setDashboard({ label: result.dashboard_title, value: result.id }); }, []); const handleClickHandler = useCallback(() => { console.log(dashboard); }, [dashboard]); return null; }; export default Expandable;
Transforms async useEffect
BEFORE
import React from 'react'; export default class Loader extends React.PureComponent { async componentDidMount() { await loadSomething(); } render() { return null; } }
AFTER
import React, { useEffect } from 'react'; const Loader = () => { useEffect(() => { (async () => { await loadSomething(); })(); }, []); return null; }; export default Loader;
Converts default props
BEFORE
import React from 'react'; export class Nice extends React.Component { static defaultProps = { compact: false, title: null, renderContent() {}, }; constructor(props) { super(props); } render() { return null; } }
AFTER
import React from 'react'; export const Nice = (inputProps) => { const props = { compact: false, title: null, renderContent: function () {}, ...inputProps, }; return null; };
Converts aliased default props
BEFORE
import React from 'react'; const defaultProps = { compact: false, title: null, renderContent() {}, }; export class Nice extends React.Component { static defaultProps = defaultProps; constructor(props) { super(props); } render() { return null; } }
AFTER
import React from 'react'; const defaultProps = { compact: false, title: null, renderContent() {}, }; export const Nice = (inputProps) => { const props = { ...defaultProps, ...inputProps, }; return null; };
Adds import from React when Component imported from elsewhere
BEFORE
import { Component } from 'base'; export default class Loader extends Component { async componentDidMount() { await loadSomething(); } render() { return null; } }
AFTER
import { Component } from 'base'; import { useEffect } from 'react'; const Loader = () => { useEffect(() => { (async () => { await loadSomething(); })(); }, []); return null; }; export default Loader;
Preserves non-return render statements
BEFORE
import { Component } from 'base'; export default class Loader extends Component { async componentDidMount() { await loadSomething(); } render() { console.log('hi'); console.info('hello'); return null; } }
AFTER
import { Component } from 'base'; import { useEffect } from 'react'; const Loader = () => { useEffect(() => { (async () => { await loadSomething(); })(); }, []); console.log('hi'); console.info('hello'); return null; }; export default Loader;
Remove method this binding
BEFORE
import { Component } from 'base'; export default class ThisBind extends Component { constructor(props){ super(props) this.sayHello = this.sayHello.bind(this) } sayHello(){ console.log("Hello!") } render() { return null; } }
AFTER
import { Component } from "base"; import { useCallback } from "react"; const ThisBind = () => { const sayHelloHandler = useCallback(() => { console.log("Hello!"); }, []); return null; }; export default ThisBind;
Keep the constructor logic even if there is not super(props)
BEFORE
import { Component } from 'base'; export default class ThisBind extends Component { constructor(){ console.log("Hello?") } render() { return null; } }
AFTER
import { Component } from "base"; const ThisBind = () => { console.log("Hello?"); return null; }; export default ThisBind;