在 React 中如何防止代码爆炸

625 阅读3分钟

Functional Component 是很棒的东西

在写了很长一段时间的 useState, useEffect 之后,发现 Functional Component 确实比 Class Component 更加方便,模糊了生命周期的概念,也没有很多令人上头 UNSAFE 方法。但是真正让我想要防止代码爆炸的其实是一条 lint 规则,就是一个方法不能超过 200 行。算上本身的逻辑代码,格式化之后的空行。其实很容易就写超过 200 行了,一个组件如果只有 200 行显然不怎么现实。如果说是要拆分子组件,在最外层组件也是会有很多 state 需要去维护,所以有一个地方肯定得爆炸。

Scenario 1

假设你有一个很简单的需求,就是写一个 Switch 用来控制一些简单的文字。

那么你的代码最初可能会长成这样:

import React, {useState} from 'react';
    
const App = () => {
    const [state, setState] = useState('on');
		return <div>
      	<div>State: {state}</div>
        <div>
            <button type="button" onClick={() => {
                setState('on')
            }}>
              Turn On
            </button>
            <button type="button" onClick={() => {
                setState('off')
            }}>
              Turn Off
            </button>
            <button type="button" onClick={() => {
                state === '' ? setState('off') : setState('on')
            }}>
              Toggle
            </button>
        </div>
   </div>  
}

Scenario 2

实际上也不是不能被接受,因为这里逻辑其实并不复杂,稍微看一下也是非常简单明了的。那么接下来我们再改一个版本:

import React, {useState, useCallback} from 'react';
const App = () => {
    const [state, setState] = useState('on');

    const on = React.useCallback(() => {
        setState('on');
    }, []);
    const off = React.useCallback(() => {
        setState('off');
    }, []);
    const toggle = React.useCallback(() => {
        setState(s => (s === 'on' ? 'off' : 'on'));
    }, []);
    
    return <div>
        <div>State: {state}</div>
        <div>
            <button type="button" onClick={on}>
              Turn On
            </button>
            <button type="button" onClick={off}>
              Turn Off
            </button>
            <button type="button" onClick={toggle}>
              Toggle
            </button>
        </div>
   </div>  
}

现在再来看一下,在 View 中代码显然变少了,也更好让人理解了。逻辑与视图分离了。

much better, so far so good.

Scenario 3

接着,当一个新的需求提了出来之后,这里你需要一个输入框。

const App = () => {
    const [state, setState] = useState('on');
    const [inputState, setInputState] = React.useState('');

    const on = React.useCallback(() => {
        setState('on')
    }, [])
    const off = React.useCallback(() => {
        setState('off')
    }, [])
    const toggle = React.useCallback(() => {
        setState(s => (s === 'on' ? 'off' : 'on'))
    }, []);

    const handleInputChange = React.useCallback(e => {
        setInputState(e.target.value)
    }, []);

    const resetInput = React.useCallback(() => {
        setInputState('')
    }, []);

    return <div>
        <div>State: {state}</div>
        <div>
            <button type="button" onClick={on}>
              Turn On
            </button>
            <button type="button" onClick={off}>
              Turn Off
            </button>
            <button type="button" onClick={toggle}>
              Toggle
            </button>
        </div>
        <div>
            <label htmlFor="randomWord">Random Word</label>
            <input
              type="text"
              id="randomWord"
              onChange={handleInputChange}
              value={inputState}
            />
            <button type="button" onClick={resetInput}>
              Reset Input
            </button>
      </div>
   </div>  
}

从上面来看,我新加入的代码并不多,无非是多了一个 useState,已经两个方法。但是这仅仅是一个 input 框而已,所以如果你在多一些这样的需求,这样的组件,你的代码就炸啦。因为这样带来了一些问题:

  • 你的方法以后离 useState 会很远,你需要来回滚动你的代码才能让你脑子记住
  • 这样子的代码带来了我之前提到的问题,很容易就爆炸了,超过行数的限制
  • 以后你的心智负担会很重,如果移除一些组件的时候,会遗漏一些方法或者 state

解决方案 useCustomHook is the rescue

import React, {useState, useMemo} from 'react';

const useOnOff = () => {
    const [state, setState] = React.useState('off')
    const handlers = useMemo(() => ({
        on: () => {
            setState('on')
        },
        off: () => {
            setState('off')
        },
        toggle: () => {
            setState(s => (s === 'on' ? 'off' : 'on'))
        },
    }), []);
    return [state, handlers]
}

const useInput = () => {
  const [state, setState] = React.useState('')

  const handlers = React.useMemo(() => ({
        handleInputChange: e => {
            setState(e.target.value)
        },
        resetInput: () => {
            setState('')
        },
    }),[])
  return [state, handlers]
}

const App = () => {
    const [state, { on, off, toggle }] = useOnOff()
    const [inputState, { handleInputChange, resetInput }] = useInput()

    return <div>
        <div>State: {state}</div>
        <div>
            <button type="button" onClick={on}>
              Turn On
            </button>
            <button type="button" onClick={off}>
              Turn Off
            </button>
            <button type="button" onClick={toggle}>
              Toggle
            </button>
        </div>
        <div>
            <label htmlFor="randomWord">Random Word</label>
            <input
              type="text"
              id="randomWord"
              onChange={handleInputChange}
              value={inputState}
            />
            <button type="button" onClick={resetInput}>
              Reset Input
            </button>
      </div>
   </div>  
}

这样看起来是不是心智负担就少了很多很多!而且最重要的是,这两个 customHook 可以放到其他地方,以备不时之需,实际上 on off switch 这种在项目中是非常常见的,这样一来你就防止了代码爆炸了。

这其实是一个软件工程的非常常见的现象,如果你的心智出现了负担,那么多一层封装可以解决你的问题,如果还有,那就再来一层。