Интенсивное чтение "Написание отказоустойчивых компонентов"

JavaScript React.js

1. Введение

ЧитатьПрочтите полное руководство по использованию эффектовПосле этого углубилось ли ваше понимание функционального компонента?

Прошел на этот разWriting Resilient ComponentsВ этой статье вы узнаете, что такое устойчивый компонент и почему функциональный компонент может это сделать.

2. Обзор

По сравнению с Lint или Prettier кода, возможно, нам следует уделять больше внимания эластичности кода.

DanОбобщаются четыре характеристики упругих компонентов:

  1. Не блокируйте потоку данных.
  2. Всегда будьте готовы к рендерингу.
  3. Не используйте одноэлементные компоненты.
  4. Изолировать локальное состояние.

Приведенные выше правила применимы не только к React, но и ко всем компонентам пользовательского интерфейса.

Не блокируйте поток данных рендеринга

Смысл не блокировать поток данных заключается в том, чтоНе локализовать полученные параметры,илиСделайте компонент полностью контролируемым.

В синтаксисе компонента классаВ соответствии с концепцией жизненного цикла в определенном жизненном циклеpropsхранить вstateметод не редкость.Однако, как толькоpropsзатвердевший доstate, компонент вышел из-под контроля:

class Button extends React.Component {
  state = {
    color: this.props.color
  };
  render() {
    const { color } = this.state; // 🔴 `color` is stale!
    return <button className={"Button-" + color}>{this.props.children}</button>;
  }
}

Когда компонент снова обновится,props.colorизменилось, ноstate.colorЭто не изменится, такая ситуация заблокирует поток данных, а друзья могут пожаловаться, что в компоненте есть баги. В это время, если вы попытаетесь пройти другие жизненные циклы (componentWillReceivePropsилиcomponentDidUpdate) исправить, код становится неуправляемым.

Однако функциональный компонент не имеет концепции жизненного цикла.Так что нет необходимостиpropsхранить вstate, вы можете визуализировать напрямую:

function Button({ color, children }) {
  return (
    // ✅ `color` is always fresh!
    <button className={"Button-" + color}>{children}</button>
  );
}

если необходимоpropsобработка, можно использоватьuseMemoКэшируйте процесс и выполняйте его повторно только при изменении зависимостей:

const textColor = useMemo(
  () => slowlyCalculateTextColor(color),
  [color] // ✅ Don’t recalculate until `color` changes
);

Не блокируйте поток данных о побочных эффектах

Отправка запроса является побочным эффектом, если запрос отправляется внутри компонента, то лучше снова получить данные при изменении параметра выборки.

class SearchResults extends React.Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.fetchResults();
  }
  componentDidUpdate(prevProps) {
    if (prevProps.query !== this.props.query) {
      // ✅ Refetch on change
      this.fetchResults();
    }
  }
  fetchResults() {
    const url = this.getFetchUrl();
    // Do the fetching...
  }
  getFetchUrl() {
    return "http://myapi/results?query" + this.props.query; // ✅ Updates are handled
  }
  render() {
    // ...
  }
}

Если реализовано в виде компонента класса, нам нужно запросить функциюgetFetchUrlвытянуть и вcomponentDidMountа такжеcomponentDidUpdateПри одновременном вызове также обратите внимание наcomponentDidUpdateКогда принимается параметрstate.queryБез изменений, без исполненияgetFetchUrl.

Такой опыт сопровождения очень плохой, если количество параметров увеличитьstate.currentPage, вы наверное вcomponentDidUpdateотсутствует правоstate.currentPageсуждение.

Если вы используете функциональный компонент, вы можете передатьuseCallbackВозьмем весь процесс получения в целом:

оригинал не б/уuseCallback, обработал автор.

function SearchResults({ query }) {
  const [data, setData] = useState(null);
  const [currentPage, setCurrentPage] = useState(0);

  const fetchResults = useCallback(() => {
    return "http://myapi/results?query" + query + "&page=" + currentPage;
  }, [currentPage, query]);

  useEffect(() => {
    const url = getFetchUrl();
    // Do the fetching...
  }, [getFetchUrl]); // ✅ Refetch on change

  // ...
}

Пара функциональных компонентовpropsа такжеstateВсе данные обрабатываются одинаково, и логика поиска номера и «оценка обновления» могут быть пропущены черезuseCallbackПолностью инкапсулируется в функцию, которая затем добавляется как монолитная зависимость кuseEffect, если в будущем будет добавлен новый параметр, просто изменитеfetchResultsЭтой функции достаточно, и ее тоже можно передать черезeslint-plugin-react-hooksУпускает ли статический анализ плагина зависимости.

Функциональный компонент не только агрегирует зависимости, но также решает проблему невозможности статического анализа зависимостей, вызванную функциональным суждением Классового компонента, разбросанного по нескольким жизненным циклам.

Не блокируйте поток данных для оптимизации производительности

в сравнении сPureComponentа такжеReact.memo, выполнять оптимизацию сравнения вручную небезопасно, например, вы можете забыть сравнить функцию:

class Button extends React.Component {
  shouldComponentUpdate(prevProps) {
    // 🔴 Doesn't compare this.props.onClick
    return this.props.color !== prevProps.color;
  }
  render() {
    const onClick = this.props.onClick; // 🔴 Doesn't reflect updates
    const textColor = slowlyCalculateTextColor(this.props.color);
    return (
      <button
        onClick={onClick}
        className={"Button-" + this.props.color + " Button-text-" + textColor}
      >
        {this.props.children}
      </button>
    );
  }
}

Приведенный выше код выполняется вручнуюshouldComponentUpdateОптимизировать контраст, но игнорировать параметры функцииonClickконтраст, поэтому, хотя большую часть времениonClickИзменений действительно нет, поэтому код не глючит:

class MyForm extends React.Component {
  handleClick = () => {
    // ✅ Always the same function
    // Do something
  };
  render() {
    return (
      <>
        <h1>Hello!</h1>
        <Button color="green" onClick={this.handleClick}>
          Press me
        </Button>
      </>
    );
  }
}

Но однажды это реализовано по-другомуonClick, ситуация отличается, например, следующие две ситуации:

class MyForm extends React.Component {
  state = {
    isEnabled: true
  };
  handleClick = () => {
    this.setState({ isEnabled: false });
    // Do something
  };
  render() {
    return (
      <>
        <h1>Hello!</h1>
        <Button
          color="green"
          onClick={
            // 🔴 Button ignores updates to the onClick prop
            this.state.isEnabled ? this.handleClick : null
          }
        >
          Press me
        </Button>
      </>
    );
  }
}

onClickнаугадnullа такжеthis.handleClickпереключаться между.

drafts.map(draft => (
  <Button
    color="blue"
    key={draft.id}
    onClick={
      // 🔴 Button ignores updates to the onClick prop
      this.handlePublish.bind(this, draft.content)
    }
  >
    Publish
  </Button>
));

еслиdraft.contentизменилось, тоonClickменяется функция.

То есть, если подкомпонент оптимизирован вручную, если сравнение функций опущено, очень вероятно, что старая функция будет выполняться, что приведет к неправильной логике.

Поэтому постарайтесь не оптимизировать его самостоятельно, а в среде Function Component функции, объявленные внутри, каждый раз имеют разные ссылки, поэтому легко найти логические ошибки, и в то же время использоватьuseCallbackа такжеuseContextпомогает решить эту проблему.

Всегда готов к рендерингу

Убедитесь, что ваши компоненты могут быть перерисованы в любое время, не вызывая ошибок во внутреннем управлении состоянием.

Для этого на самом деле викторины, такие как сложный компонент, если состояние получено в качестве отправной точки, последующий код основан на этой отправной точке, чтобы получить много внутреннего состояния, изменить это начальное значение в данный момент, и компонент также может работать нормально.

Например следующий код:

// 🤔 Should prevent unnecessary re-renders... right?
class TextInput extends React.PureComponent {
  state = {
    value: ""
  };
  // 🔴 Resets local state on every parent render
  componentWillReceiveProps(nextProps) {
    this.setState({ value: nextProps.value });
  }
  handleChange = e => {
    this.setState({ value: e.target.value });
  };
  render() {
    return <input value={this.state.value} onChange={this.handleChange} />;
  }
}

componentWillReceivePropsИдентифицирует каждый раз, когда компонент получает новыйprops, будетprops.valueсинхронизировать сstate.value. это производноеstate, хотя, похоже, это можно сделать изящноpropsменяется, ноПеререндеринг родительского элемента по другим причинам вызоветstate.valueАномальный сброс, такой как родительский элементforceUpdate.

Конечно черезНе блокируйте поток данных рендерингаспособом, описанным в разделе, напримерPureComponent, shouldComponentUpdate, React.memoдля оптимизации производительности (когдаprops.valueне сбрасывается, если нет измененийstate.value), но такой код все еще хрупок.

Надежный код не содержит ошибок только потому, что оптимизация удалена, не используйте деривациюstateЭтой проблемы можно избежать.

Автор добавляет: Способ решения этой проблемы: 1. Если компонент зависит отprops.value, вам не нужно использоватьstate.value, полныйУправляемые компоненты. 2. При необходимостиstate.value, то сделать его внутренним состоянием, то есть не получать его извнеprops.value. Короче говоря, избегайте написания «компонентов между контролируемым и неконтролируемым».

Чтобы добавить, если вы делаете неуправляемый компонент, но хотите сбросить начальное значение, то добавьте его в родительский вызовkeyрешать:

<EmailInput defaultEmail={this.props.user.email} key={this.props.user.id} />

Кроме того, черезrefРешите, пусть дочерний элемент предоставляетresetфункция, но не рекомендуетсяref.

Не иметь одноэлементных компонентов

Устойчивое приложение должно пройти следующие тесты:

ReactDOM.render(
  <>
    <MyApp />
    <MyApp />
  </>,
  document.getElementById("root")
);

Визуализировать все приложение дважды, чтобы убедиться, что каждое из них работает правильно?

В дополнение к тому, что компонентное локальное состояние поддерживается локально, устойчивый компонент не должен «пропустить некоторые состояние или функциональность навсегда» только потому, что другие экземпляры вызывают некоторые функции.

Автор добавляет: опасный компонент вообще думает: никто не будет уничтожать поток данных по своему желанию, так что пока он естьdidMountа такжеunMountПри выполнении инициализации и уничтожения данных на линии.

Затем, когда другой экземпляр выполняет операцию уничтожения, промежуточное состояние этого экземпляра может быть уничтожено. Устойчивый компонент должен иметь возможностьОтвечайте в любое времяИзменения в состоянии, функциональный компонент без концепции жизненного цикла, очевидно, более удобен для работы.

Изолировать локальное состояние

Часто бывает трудно определить, относятся ли данные к локальному состоянию компонента или к глобальному состоянию.

В статье представлен метод суждения:«Представьте, что этот компонент отображает два экземпляра одновременно, влияют ли эти данные на оба экземпляра? Если ответ отрицательный, то эти данные подходят в качестве локального состояния».

Особенно при написании бизнес-компонентов легко спутать бизнес-данные с данными состояния самого компонента.

По опыту автора,От бизнеса верхнего уровня к базовым общим компонентам количество локальных состояний увеличивается:

业务
  -> 全局数据流
    -> 页面(完全依赖全局数据流,几乎没有自己的状态)
      -> 业务组件(从页面或全局数据流继承数据,很少有自己状态)
        -> 通用组件(完全受控,比如 input;或大量内聚状态的复杂通用逻辑,比如 monaco-editor)

3. Интенсивное чтение

Опять же, устойчивый компонент должен одновременно удовлетворять следующим четырем принципам:

  1. Не блокируйте поток данных.
  2. Всегда будьте готовы к рендерингу.
  3. Не используйте одноэлементные компоненты.
  4. Изолировать локальное состояние.

Соблюдение этих правил не кажется сложным, но в процессе практики возникнет много проблем, приведу несколько примеров.

Частая передача функций обратного вызова

Функциональная сборка компонента может привести к тому, что относительно небольшой размер решается одновременно улучшение ремонтопригодности также вызывает глобальноеstateВ прошлом следующий код мог вызвать у вас неловкость:

const App = memo(function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("nick");

  return (
    <>
      <Count count={count} setCount={setCount}/>
      <Name name={name} setName={setName}/>
    </>
  );
});

const Count = memo(function Count(props) {
  return (
      <input value={props.count} onChange={pipeEvent(props.setCount)}>
  );
});

const Name = memo(function Name(props) {
  return (
  <input value={props.name} onChange={pipeEvent(props.setName)}>
  );
});

В то время как подкомпонентыCountа такжеNameРазделяя его, логика более развязана, но дочернему компоненту становится проблематично обновлять состояние родительского компонента, мы не хотим передавать функцию в качестве параметра дочернему компоненту.

Один из способов - передать функцию черезContextПередать дочернему компоненту:

const SetCount = createContext(null)
const SetName = createContext(null)

const App = memo(function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("nick");

  return (
    <SetCount.Provider value={setCount}>
      <SetName.Provider value={setName}>
        <Count count={count}/>
        <Name name={name}/>
      </SetName.Provider>
    </SetCount.Provider>
  );
});

const Count = memo(function Count(props) {
  const setCount = useContext(SetCount)
  return (
      <input value={props.count} onChange={pipeEvent(setCount)}>
  );
});

const Name = memo(function Name(props) {
  const setName = useContext(SetName)
  return (
  <input value={props.name} onChange={pipeEvent(setName)}>
  );
});

Но это приведет кProviderСлишком раздут, поэтому рекомендуется использовать некоторые компонентыuseReducerзаменятьuseState, который объединяет функцию вdispatch:

const AppDispatch = createContext(null)

class State = {
  count = 0
  name = 'nick'
}

function appReducer(state, action) {
  switch(action.type) {
    case 'setCount':
      return {
        ...state,
        count: action.value
      }
    case 'setName':
      return {
        ...state,
        name: action.value
      }
    default:
      return state
  }
}

const App = memo(function App() {
  const [state, dispatch] = useReducer(appReducer, new State())

  return (
    <AppDispatch.Provider value={dispaych}>
      <Count count={count}/>
      <Name name={name}/>
    </AppDispatch.Provider>
  );
});

const Count = memo(function Count(props) {
  const dispatch = useContext(AppDispatch)
  return (
      <input value={props.count} onChange={pipeEvent(value => dispatch({type: 'setCount', value}))}>
  );
});

const Name = memo(function Name(props) {
  const dispatch = useContext(AppDispatch)
  return (
  <input value={props.name} onChange={pipeEvent(pipeEvent(value => dispatch({type: 'setName', value})))}>
  );
});

Совокупное состояние дляreducer, такойContextProviderОн решает все проблемы с обработкой данных.

Компонент, обернутый мемо, аналогичен эффекту PureComponent.

Параметры useCallback часто меняются

существуетПрочтите полное руководство по использованию эффектовМы вводим использованиеuseCallbackСоздайте неизменяемую функцию:

function Form() {
  const [text, updateText] = useState("");

  const handleSubmit = useCallback(() => {
    const currentText = text;
    alert(currentText);
  }, [text]);

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

Но зависимость этой функции[text]меняется так часто, что каждыйrenderбудет восстановленhandleSubmitфункция, которая оказывает определенное влияние на производительность. Одним из решений является использованиеRefЧтобы обойти эту проблему:

function Form() {
  const [text, updateText] = useState("");
  const textRef = useRef();

  useEffect(() => {
    textRef.current = text; // Write it to the ref
  });

  const handleSubmit = useCallback(() => {
    const currentText = textRef.current; // Read it from the ref
    alert(currentText);
  }, [textRef]); // Don't recreate handleSubmit like [text] would do

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

Конечно, вы также можете инкапсулировать этот процесс в виде пользовательских хуков, чтобы код выглядел немного лучше:

function Form() {
  const [text, updateText] = useState("");
  // Will be memoized even if `text` changes:
  const handleSubmit = useEventCallback(() => {
    alert(text);
  }, [text]);

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

function useEventCallback(fn, dependencies) {
  const ref = useRef(() => {
    throw new Error("Cannot call an event handler while rendering.");
  });

  useEffect(() => {
    ref.current = fn;
  }, [fn, ...dependencies]);

  return useCallback(() => {
    const fn = ref.current;
    return fn();
  }, [ref]);
}

Однако это решение не является элегантным.React предлагает более элегантное решение.

потенциально злоупотребляемый useReducer

существуетПрочтите полное руководство по использованию эффектовКак упоминалось в разделе «Отвязка обновлений от действий», используйтеuseReducerРешите «проблему, что функции зависят от нескольких внешних переменных одновременно».

Обычно мы будем использоватьuseReducer:

const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return { value: state.value + 1 };
    case "decrement":
      return { value: state.value - 1 };
    case "incrementAmount":
      return { value: state.value + action.amount };
    default:
      throw new Error();
  }
};

const [state, dispatch] = useReducer(reducer, { value: 0 });

Но на самом делеuseReducerправильноstateа такжеactionможет быть определено произвольно, поэтому мы можем использоватьuseReducerпостроитьuseState.

Например, мы создаем сложный ключ сuseState:

const [state, setState] = useState({ count: 0, name: "nick" });

// 修改 count
setState(state => ({ ...state, count: 1 }));

// 修改 name
setState(state => ({ ...state, name: "jack" }));

использоватьuseReducerРеализовать аналогичный функционал:

function reducer(state, action) {
  return action(state);
}

const [state, dispatch] = useReducer(reducer, { count: 0, name: "nick" });

// 修改 count
dispatch(state => ({ ...state, count: 1 }));

// 修改 name
dispatch(state => ({ ...state, name: "jack" }));

Следовательно, для вышеуказанной ситуации мы можем злоупотреблятьuseReducer, рекомендуется использовать непосредственноuseStateзаменять.

4. Резюме

В этой статье представлены четыре характеристики отказоустойчивых компонентов: не блокировать поток данных, всегда быть готовым к отрисовке, не иметь одноэлементных компонентов и изолировать локальное состояние.

Это соглашение важно для качества кода, и его трудно определить с помощью правил lint или простого визуального наблюдения, поэтому его трудно обобщать.

В целом функциональный компонент обеспечивает более элегантный код, но также требует более тесного взаимодействия в команде.

Адрес обсуждения:Интенсивное чтение «Написание отказоустойчивых компонентов» · Выпуск №139 · dt-fe/weekly

Если вы хотите принять участие в обсуждении, пожалуйста,кликните сюда, с новыми темами каждую неделю, выходящими по выходным или понедельникам. Интерфейс интенсивного чтения — поможет вам отфильтровать надежный контент.

Сфокусируйся наАккаунт WeChat для интенсивного чтения в интерфейсе

special Sponsors

Заявление об авторских правах: Бесплатная перепечатка - некоммерческая - не производная - сохранить авторство (Лицензия Creative Commons 3.0)