Я большой фанат новых API хуков. Но знаете, у них есть странные ограничения по применению. Для тех, кто жаждет понять причины появления этих правил, здесь я представлю модель понимания, того, как их применять.
Предупреждаю: Хуки сейчас в статусе эксперимента
Эта статья об API хуков, который на данный момент находится в тестовом режиме. Документацию по стабильной версии React API можно найти здесь.
Как работают хуки?
Я слышал о борьбе некоторых людей с предложенным проектом API хуков. Поэтому здесь я попытаюсь разъяснить, как работает предложенный синтаксис, по крайней мере, поверхностно.
Правила хуков
Есть два основных правила, упоминаемые командой React, которым вы должны следовать, используя хуки. Они описаны в документации.
- Не вызывайте хуки внутри циклов, условий или вложенных функций
- Вызывайте хуки только из функций React
Последнее, я думаю, очевидно. Чтобы назначить поведение функциональному компоненту, нужно иметь возможность как-то ассоциировать это поведение с этим компонентом.
А вот первый пункт может сбить с толку. Это кажется не естественным, для программ, использующих подобные API и именно об этом я и хочу поговорить.
Управление состояниями в хуках — всё дело в массивах
Чтобы внести ясность в эту модель понимания, давайте посмотрим на простейшую реализацию API хуков.
Пожалуйста имейте в виду, что это всего лишь рассуждения и один из возможных способов реализации API, чтобы показать вам, как можно относиться к хукам. Речь пойдёт не о внутреннем устройстве API. Более того, это не окончательная реализация хуков, в будущем всё может измениться.
Как можно реализовать `useState()` ?
Давайте разберём пример, чтобы продемонстрировать реализацию state hook.
Начнём с компонента:
function RenderFunctionComponent() { const [firstName, setFirstName] = useState("Rudi"); const [lastName, setLastName] = useState("Yardley"); return ( <Button onClick={() => setFirstName("Fred")}>Fred</Button> ); }
Идея API хуков в том, что вы можете использовать сеттер-функцию, возвращённую в качестве второго элемента массива из хуки-функции. Этот сеттер будет контролировать state, под управлением хуков.
Итак, что же React будет с этим делать?
Давайте разберёмся, как это будет работать внутри React. Этот код будет работать в контексте исполнения, для отображения определённого компонента. Это значит, что данные, которые здесь хранятся, «живут» за пределами этого компонента. Этот state не доступен другим компонентам, но поддерживается в пространстве, которое доступно для дальнейшего рендеринга определённого компонента.
1) Инициализация
Создаём два пустых массива: `setters` и `state`
Устанавливаем курсор на 0
2) Первый рендер
Запускаем функцию компонента в первый раз.
С каждым вызовом useState()
, при первом запуске, в массив setters
записывается сеттер-функция (с привязкой к позиции курсора),
а затем state в массив state
.
3) Последующий рендер
Каждый последующий рендер сбрасывает курсор, а те значения просто считываются из каждого массива.
4) Обработка событий
Каждый сеттер ссылается на свою позицию курсора. Поэтому вызов любого setter
элемента будет менять значение state, в массиве state, в соответствии с позицией курсора.
«Наивная» реализация
Этот пример демонстрирует «наивную» реализацию:
let state = []; let setters = []; let firstRun = true; let cursor = 0; function createSetter(cursor) { return function setterWithCursor(newVal) { state[cursor] = newVal; }; } // This is the pseudocode for the useState helper export function useState(initVal) { if (firstRun) { state.push(initVal); setters.push(createSetter(cursor)); firstRun = false; } const setter = setters[cursor]; const value = state[cursor]; cursor++; return [value, setter]; } // Our component code that uses hooks function RenderFunctionComponent() { const [firstName, setFirstName] = useState("Rudi"); // cursor: 0 const [lastName, setLastName] = useState("Yardley"); // cursor: 1 return ( <div> <Button onClick={() => setFirstName("Richard")}>Richard</Button> <Button onClick={() => setFirstName("Fred")}>Fred</Button> </div> ); } // This is sort of simulating Reacts rendering cycle function MyComponent() { cursor = 0; // resetting the cursor return <RenderFunctionComponent />; // render } console.log(state); // Pre-render: [] MyComponent(); console.log(state); // First-render: ['Rudi', 'Yardley'] MyComponent(); console.log(state); // Subsequent-render: ['Rudi', 'Yardley'] // click the 'Fred' button console.log(state); // After-click: ['Fred', 'Yardley']
Почему порядок так важен
Вопрос: что случится если мы изменим порядок хуков для цикла рендера, основываясь на некоем внешнем факторе или даже состоянии компонента?
Давайте проделаем то, что команда React не рекомендует делать:
let firstRender = true; function RenderFunctionComponent() { let initName; if(firstRender){ [initName] = useState("Rudi"); firstRender = false; } const [firstName, setFirstName] = useState(initName); const [lastName, setLastName] = useState("Yardley"); return ( <Button onClick={() => setFirstName("Fred")}>Fred</Button> ); }
Здесь у нас вызов useState
находится внутри условия. Посмотрите к какому беспорядку это привело.
Плохой компонент. Первый рендер
На данный момент, экземпляры переменных firstName
и lastName
содержат верные данные, но посмотрите, что произойдёт на втором рендере:
Плохой компонент. Второй рендер
Теперь firstName
и lastName
содержат “Rudi”
, так как state хранилище стало непоследовательным. Это не рабочий пример с явной ошибкой, но он даёт понимание, почему появились эти правила для хуков.
Команда React оговаривает правила использования, потому что их несоблюдение приведёт к несогласованности данных
Думайте о хуках, как о манипуляциях с массивами и не нарушайте правил
Теперь должно быть понятно, почему нельзя вызывать `use` хуки внутри условий и циклов. Потому что у нас будут проблемы с курсором, указывающим на соответствие в массивах. Если изменить порядок вызовов в течение рендера, курсор не будет указывать на верные данные или обработчики.
Управляйте хуками, думая о массивах, которым нужен курсор в правильном месте.
Заключение
Надеюсь, я смог пролить свет на то, что происходит под капотом API хуков. Помните о том, что истинная ценность здесь заключается в возможности группировать задачи. Поэтому внимательно относитесь к порядку и использованию API хуков. Это будет вознаграждено.
Hooks — это эффективный API плагин для React Components. Некоторые разработчики в восторге от его появления и на то есть причины. Если вы будете думать о состояниях, как о наборе массивов, то у вас не возникнет проблем с правилами React.
В будущем, надеюсь увидеть метод useEffects
и сравнить его с методами жизненного цикла компонентов React.
Перевод статьи Rudi Yardley : React hooks: not magic, just arrays