React 컴포넌트의 7가지 코드 냄새

42218 단어 reactwebdevjavascript
내가 React 컴포넌트에서 코드 냄새라고 생각하는 것들이 점점 늘어나고 있습니다.
  • Too many props
  • Incompatible props
  • Copying props into state
  • Returning JSX from functions
  • Multiple booleans for state
  • Too many useState in a component
  • Large useEffect

  • 너무 많은 소품

    Passing too many props into a single component may be a sign that the component should be split up.

    How many are too many you ask? Well.. "it depends". You might find yourself in a situation where a component have 20 props or more, and still be satisfied that it only does one thing. But when you do stumble upon a component that has many props or you get the urge to add just one more to the already long list of props there's a couple of things to consider:

    이 구성 요소가 여러 작업을 수행합니까?



    함수와 마찬가지로 구성 요소는 한 가지 일을 잘 수행해야 하므로 구성 요소를 여러 개의 작은 구성 요소로 분할할 수 있는지 항상 확인하는 것이 좋습니다. 예를 들어 구성 요소에 incompatible props 또는 returns JSX from functions 이 있는 경우 .

    구성을 사용할 수 있습니까?



    매우 훌륭하지만 종종 간과되는 패턴은 하나의 내부에서 모든 논리를 처리하는 대신 구성 요소를 구성하는 것입니다. 어떤 조직에 대한 사용자 애플리케이션을 처리하는 구성 요소가 있다고 가정해 보겠습니다.

    <ApplicationForm
      user={userData}
      organization={organizationData}
      categories={categoriesData}
      locations={locationsData}
      onSubmit={handleSubmit}
      onCancel={handleCancel}
      ...
    />
    


    이 구성 요소의 props를 보면 모든 구성 요소가 구성 요소가 하는 일과 관련되어 있음을 알 수 있지만 대신 일부 구성 요소 책임을 자식에게 이전하여 이를 개선할 여지가 있습니다.

    <ApplicationForm onSubmit={handleSubmit} onCancel={handleCancel}>
      <ApplicationUserForm user={userData} />
      <ApplicationOrganizationForm organization={organizationData} />
      <ApplicationCategoryForm categories={categoriesData} />
      <ApplicationLocationsForm locations={locationsData} />
    </ApplicationForm>
    


    이제 ApplicationForm 양식 제출 및 취소와 같은 가장 좁은 책임만 처리하도록 했습니다. 자식 구성 요소는 더 큰 그림에서 해당 부분과 관련된 모든 것을 처리할 수 있습니다. 이것은 또한 자녀와 부모 간의 의사 소통을 위해 React Context를 사용할 수있는 좋은 기회입니다.

    많은 '구성' 소품을 전달하고 있습니까?



    어떤 경우에는 소품을 옵션 개체로 그룹화하는 것이 좋습니다. 예를 들어 이 구성을 쉽게 바꿀 수 있습니다. 일종의 그리드나 테이블을 표시하는 구성 요소가 있는 경우:

    <Grid
      data={gridData}
      pagination={false}
      autoSize={true}
      enableSort={true}
      sortOrder="desc"
      disableSelection={true}
      infiniteScroll={true}
      ...
    />
    

    data를 제외한 이러한 모든 소품은 구성으로 간주될 수 있습니다. 이와 같은 경우에는 Grid 소품을 대신 수락하도록 options를 변경하는 것이 좋습니다.

    const options = {
      pagination: false,
      autoSize: true,
      enableSort: true,
      sortOrder: 'desc',
      disableSelection: true,
      infiniteScroll: true,
      ...
    }
    
    <Grid
      data={gridData}
      options={options}
    />
    


    이것은 또한 서로 다른 options 간에 교체하는 경우 사용하지 않으려는 구성 옵션을 제외하는 것이 더 쉽다는 것을 의미합니다.

    호환되지 않는 소품

    Avoid passing props that are incompatible with each other.

    For instance, we might start by creating a common <Input /> component that is intended to just handle text, but after a while we also add the possibility to use it for phone numbers as well. The implementation could look something like this:

    function Input({ value, isPhoneNumberInput, autoCapitalize }) {
      if (autoCapitalize) capitalize(value)
    
      return <input value={value} type={isPhoneNumberInput ? 'tel' : 'text'} />
    }
    

    The problem with this is that the props isPhoneNumberInput and autoCapitalize don't make sense together. We can't really capitalize phone numbers.

    In this case the solution is probably to break the component up into multiple smaller components. If we still have some logic we want to share between them, we can move it to a custom hook :

    function TextInput({ value, autoCapitalize }) {
      if (autoCapitalize) capitalize(value)
      useSharedInputLogic()
    
      return <input value={value} type="text" />
    }
    
    function PhoneNumberInput({ value }) {
      useSharedInputLogic()
    
      return <input value={value} type="tel" />
    }
    


    이 예제는 약간 인위적이지만 서로 호환되지 않는 props를 찾는 것은 일반적으로 구성 요소를 분리해야 하는지 확인해야 한다는 좋은 표시입니다.

    props를 state로 복사하기

    Don't stop the data flow by copying props into state.

    Consider this component:

    function Button({ text }) {
      const [buttonText] = useState(text)
    
      return <button>{buttonText}</button>
    }
    

    By passing the text prop as the initial value of useState the component now practically ignores all updated values of text . If the text prop was updated the component would still render its first value. For most props this is unexpected behavior which in turn makes the component more bug-prone.

    A more practical example of this happening is when we want to derive some new value from a prop and especially if this requires some slow calculation. In the example below, we run the slowlyFormatText function to format our text -prop, which takes a lot of time to execute.

    function Button({ text }) {
      const [formattedText] = useState(() => slowlyFormatText(text))
    
      return <button>{formattedText}</button>
    }
    
    By putting it into state we've solved the issue that it will rerun unnecessarily but like above we've also stopped the component from updating. A better way to solving this issue is using the useMemo hook 결과를 메모하기:

    function Button({ text }) {
      const formattedText = useMemo(() => slowlyFormatText(text), [text])
    
      return <button>{formattedText}</button>
    }
    


    이제 slowlyFormatTexttext가 변경되고 구성 요소 업데이트를 중지하지 않은 경우에만 실행됩니다.

    Sometimes we do need a prop where all updates to it are ignored, e.g. a color picker where we need the option to set an initially picked color but when the user has picked a color we don't want an update to override the users choice. In this case it's totally fine to copy the prop into state, but to indicate this behavior to the user most developers prefix the prop with either initial or default (initialColor/defaultColor).



    추가 자료: Writing resilient components by Dan Abramov .

    함수에서 JSX 반환

    Don't return JSX from functions inside a component.

    This is a pattern that has largely disappeared when function components became more popular, but I still run into it from time to time. Just to give an example of what I mean:

    function Component() {
      const topSection = () => {
        return (
          <header>
            <h1>Component header</h1>
          </header>
        )
      }
    
      const middleSection = () => {
        return (
          <main>
            <p>Some text</p>
          </main>
        )
      }
    
      const bottomSection = () => {
        return (
          <footer>
            <p>Some footer text</p>
          </footer>
        )
      }
    
      return (
        <div>
          {topSection()}
          {middleSection()}
          {bottomSection()}
        </div>
      )
    }
    

    While this might feel okay at first it makes it hard to reason about the code, discourages good patterns, and should be avoided. To solve it I either inline the JSX because a large return isn't that big of a problem, but more often this is a reason to break these sections into separate components instead.

    Remember that just because you create a new component you don't have to move it to a new file as well. Sometimes it makes sense to keep multiple components in the same file if they are tightly coupled.

    상태에 대한 다중 부울

    Avoid using multiple booleans to represent a components state.

    When writing a component and subsequently extending the functionality of the component it's easy to end up in a situation where you have multiple booleans to indicate which state the component is in. For a small component that does a web request when you click a button you might have something like this:

    function Component() {
      const [isLoading, setIsLoading] = useState(false)
      const [isFinished, setIsFinished] = useState(false)
      const [hasError, setHasError] = useState(false)
    
      const fetchSomething = () => {
        setIsLoading(true)
    
        fetch(url)
          .then(() => {
            setIsLoading(false)
            setIsFinished(true)
          })
          .catch(() => {
            setHasError(true)
          })
      }
    
      if (isLoading) return <Loader />
      if (hasError) return <Error />
      if (isFinished) return <Success />
    
      return <button onClick={fetchSomething} />
    }
    

    When the button is clicked we set isLoading to true and do a web request with fetch. If the request is successful we set isLoading to false and isFinished to true and otherwise set hasError to true if there was an error.

    While this technically works fine it's hard to reason about what state the component is in and it's more error-prone than alternatives. We could also end up in an "impossible state", such as if we accidentally set both isLoading and isFinished to true at the same time.

    A better way to handle this is to manage the state with an "enum" instead. In other languages enums are a way to define a variable that is only allowed to be set to a predefined collection of constant values, and while enums don't technically exist in Javascript we can use a string as an enum and still get a lot of benefits:

    function Component() {
      const [state, setState] = useState('idle')
    
      const fetchSomething = () => {
        setState('loading')
    
        fetch(url)
          .then(() => {
            setState('finished')
          })
          .catch(() => {
            setState('error')
          })
      }
    
      if (state === 'loading') return <Loader />
      if (state === 'error') return <Error />
      if (state === 'finished') return <Success />
    
      return <button onClick={fetchSomething} />
    }
    

    By doing it this way we've removed the possibility for impossible states and made it much easier to reason about this component. Finally, if you're using some sort of type system like TypeScript it's even better since you can specify the possible states:

    const [state, setState] = useState<'idle' | 'loading' | 'error' | 'finished'>('idle')
    

    너무 많은 useState

    Avoid using too many useState hooks in the same component.

    A component with many useState hooks is likely doing Too Many Things™️ and probably a good candidate for breaking into multiple components, but there are also some complex cases where we need to manage some complex state in a single component.

    Here's an example of how some state and a couple of functions in an autocomplete input component could look like:

    function AutocompleteInput() {
      const [isOpen, setIsOpen] = useState(false)
      const [inputValue, setInputValue] = useState('')
      const [items, setItems] = useState([])
      const [selectedItem, setSelectedItem] = useState(null)
      const [activeIndex, setActiveIndex] = useState(-1)
    
      const reset = () => {
        setIsOpen(false)
        setInputValue('')
        setItems([])
        setSelectedItem(null)
        setActiveIndex(-1)
      }
    
      const selectItem = (item) => {
        setIsOpen(false)
        setInputValue(item.name)
        setSelectedItem(item)
      }
    
      ...
    }
    

    We have a reset function that resets all of the state and a selectItem function that updates some of our state. These functions both have to use quite a few state setters from all of our useState s to do their intended task. Now imagine that we have a lot of more actions that have to update the state and it's easy to see that this becomes hard to keep bug free in the long run. In these cases it can be beneficial to manage our state with a useReducer hook instead:

    const initialState = {
      isOpen: false,
      inputValue: "",
      items: [],
      selectedItem: null,
      activeIndex: -1
    }
    function reducer(state, action) {
      switch (action.type) {
        case "reset":
          return {
            ...initialState
          }
        case "selectItem":
          return {
            ...state,
            isOpen: false,
            inputValue: action.payload.name,
            selectedItem: action.payload
          }
        default:
          throw Error()
      }
    }
    
    function AutocompleteInput() {
      const [state, dispatch] = useReducer(reducer, initialState)
    
      const reset = () => {
        dispatch({ type: 'reset' })
      }
    
      const selectItem = (item) => {
        dispatch({ type: 'selectItem', payload: item })
      }
    
      ...
    }
    

    By using a reducer we've encapsulated the logic for managing our state and moved the complexity out of our component. This makes it much easier to understand what is going on now that we can think about our state and our component separately.

    Both useState and useReducer come with their pros, cons and different use cases (pun intended). One of my favorites with reducers is the state reducer pattern by Kent C. Dodds.



    큰 사용효과

    Avoid large useEffect s that do multiple things. They make your code error-prone and harder to reason about.

    A mistake that I made a lot when hooks were released was putting too many things into a single useEffect . To illustrate, here's a component with a single useEffect :

    function Post({ id, unlisted }) {
      ...
    
      useEffect(() => {
        fetch(`/posts/${id}`).then(/* do something */)
    
        setVisibility(unlisted)
      }, [id, unlisted])
    
      ...
    }
    

    While this effect isn't that large it still do multiple things. When the unlisted prop changes we will fetch the post even if id hasn't changed.

    To catch errors like this I try to describe the effects I write by saying "when [dependencies] change do this" to myself. Applying that to the effect above we get "when id or unlisted changes, fetch the post and update visibility". If this sentence contains the words "or" or "and" it usually points to a problem.

    Breaking this effect up into two effects instead:

    function Post({ id, unlisted }) {
      ...
    
      useEffect(() => { // when id changes fetch the post
        fetch(`/posts/${id}`).then(/* ... */)
      }, [id])
    
      useEffect(() => { // when unlisted changes update visibility
        setVisibility(unlisted)
      }, [unlisted])
    
      ...
    }
    

    By doing this we've reduced the complexity of our component, made it easier to reason about and lowered the risk of creating bugs.

    마무리



    좋아, 지금은 그게 다야! 이것은 어떤 방법으로든 규칙이 아니라 무언가가 "잘못"될 수 있다는 신호라는 것을 기억하십시오. 합당한 이유로 위의 작업 중 일부를 수행하려는 상황에 직면하게 될 것입니다.

    내가 왜 이것에 대해 매우 틀렸는지에 대한 피드백이 있습니까? 구성 요소에서 발견한 다른 코드 냄새에 대한 제안은 무엇입니까? 댓글을 작성하거나 저를 누르십시오!

    좋은 웹페이지 즐겨찾기