Галоўная / Артыкулы / Проп-дрілінг не ўтварае праблемы для інсталляціі Redux чыра Zustand.

Проп-дрілінг не ўтварае праблемы для інсталляціі Redux чыра Zustand.

Тэст чатыро распашчых прычын дадзення дзяржавной бібліятэкі ў рамках працуючага коду на React: Context для праходжання через пропы, useState, useSyncExternalStore і выклік перэрасчытвання элементаў ад Context.

2370 слоў

Запытайце каманду, чаму ў іхнім дапрацоўкі на React викорыстоўваецца Redux, Zustand чы MobX, і зазвычай першай адказаець проблема prop drilling. Хоць гэта і ўскладненне, гэта не є правым адказам для дадавання залежнасці, так сама як і тры адказы, якія зазвычай следуюць за гэтым. Нижэй кожны з эўтах чатырох адказаў перапрацаваны на прыкладзе маленькага работячага прыкладу, разам з тым, што React вже надае для такога случая, і ситуацыяю, калі бібліятэка для керавання станам дзеясна компенсуе сабе сваю вартасць.

Чатыры распашчастыя адказы

Аргументы зазвычай прыходзяць у прагнозаванай парадку:

  • Перадача значэння через компоненты, якія яго не выкарыстоўваюць, ёсць клопатна, таму дапрацоўкі патрэбны магазін стану.
  • Кліентскі стан, які дзейсна зміняецца, напрыклад, флаг светлай чы темнай тэматыки, безумовна патрэбуе бібліятэкі.
  • Ёжыць кожны іншы компонент, які чытае гэты стан, каб ён автаматычна апошнаваўся, без ручной наладкі пропаў, неабходна викорыстоўваць такую бібліятэку.
  • Якщо цэлая бібліятэка здаецца занадта великай, Context являецца безпечным прыемнікам.
  • Każда з іх перестае функцыянацыяваць, калі адзін раз паглядзеце на конкрэтны код.

    Prop drilling: проблема, якую React рашыў ўжо калісь

    Prop drilling означае перадачу значэння через калькі слоёў выключна для таго, каб глыбокая вкладзеная складовая могла яго прачытаць, пры чым жадны з проміжных слоёў яго не выкарыстоўвае. Чатыры маленькія файлы паказваюць структуру. App зберагае об’ект user у стане і перадае яго Dashboard.

    // App.jsx
    import { useState } from 'react';
    import Dashboard from './components/Dashboard';
    
    function App() {
      const [user, setUser] = useState({ name: 'Akshat', avatarUrl: '/me.png' });
      return <Dashboard user={user} />;
    }
    export default App;
    

    Dashboard нічога не робіць з user, аднак перадае яго далей Sidebar.

    // components/Dashboard.jsx
    import Sidebar from './Sidebar';
    
    function Dashboard({ user }) {
      return <Sidebar user={user} />;
    }
    export default Dashboard;
    

    Sidebar робіць тое ж самае, распакоўваючы два поля для Avatar.

    // components/Sidebar.jsx
    import Avatar from './Avatar';
    
    function Sidebar({ user }) {
      return <Avatar name={user.name} avatarUrl={user.avatarUrl} />;
    }
    export default Sidebar;
    

    Толькі Avatar насправды атрыбутуе данні.

    // components/Avatar.jsx
    function Avatar({ name, avatarUrl }) {
      return <img src={avatarUrl} alt={name} />;
    }
    
    export default Avatar;
    

    Ні Dashboard.jsx, ні Sidebar.jsx не выкарыстоўваюць значэнне user для сваёй логікі. Яны толькі перадаюць яго далей. Якщо avatarUrl будзе перайменаваны на photoUrl, оба файлы патрабуюць правек, навет якщо ўжо не змянілася ўсё іхае працэсаванне, проста таму, што яны перадаюць тое, чыямі не ўладаюць.

    Контэкст безпасова дае значэнне

    Вбудоўаны спосаб Рэакта для гэтага — контэкст. Спачатку об’ект контэксту ствараецца аднойчы ў своём модулі.

    // context/UserContext.js
    import { createContext } from 'react';
    
    const UserContext = createContext(null);
    export default UserContext;
    

    Потым App абгортае паддрэс у Provider контэксту і перадае user як його значэнне, замест таго каб перадаць яго як проп.

    // App.jsx
    import { useState } from 'react';
    import UserContext from './context/UserContext';
    import Dashboard from './components/Dashboard';
    
    function App() {
      const [user, setUser] = useState({ name: 'Akshat', avatarUrl: '/me.png' });
      return (
        <UserContext.Provider value={user}>
          <Dashboard />
        </UserContext.Provider>
      );
    }
    export default App;
    

    Dashboard і Sidebar ператвараюцца на чыстыя элементы макету, у якіх зовсамна не згадваецца user.

    // components/Dashboard.jsx
    import Sidebar from './Sidebar';
    
    function Dashboard() {
      return <Sidebar />;
    }
    export default Dashboard;
    
    // components/Sidebar.jsx
    import Avatar from './Avatar';
    
    function Sidebar() {
      return <Avatar />;
    }
    export default Sidebar;
    

    У падсумку Avatar сам выкарыстоўвае гэта значэнне.

    // components/Avatar.jsx
    import { useContext } from 'react';
    import UserContext from '../context/UserContext';
    
    function Avatar() {
      const user = useContext(UserContext);
      return <img src={user.avatarUrl} alt={user.name} />;
    }
    export default Avatar;
    

    Кожны з трох каналавых элементаў выконвае адну задачу. Функцыя createContext стварае канал, які не залежыць ад пропсаў. Клас Provider дазволяе доступнаць значэнню user для всіх элементаў ў яго паддрэвае адразу. Функцыя useContext(UserContext) чытае гэта значэнне з найбліжэйшага Provider, які знаходзится вышэй за компонентом, пры чым ігнаруецца кожны прыемлівы слой. Промежуточные компоненты перестаюць выкарыстоўваць user, таму што ёму ніколі не была патрэбна. Да дапамогі: у React 19 таксама можна безпасабліва адобразіць об’ект контэкста як Provider, але спосаб .Provider, паказаны тут, таксама застаецца працэспрыводным.

    Чаму аргумент пра «дрілінг пропсаў» застаўся, нават калі яго прычына зникла

    Історыя пояснюе, чаму гэты спарч выжывае. Redux з’явіўся ў чырвені 2015 года, быў створаны Дэнам Абрамовам і Эндрю Кларкам і базаваўся на архітектуре Flux від Facebook: едын цэнтральны хранар з строгімі правіламі ўсупрацоўкі змян стану. API Context у React стаў стабільным і офіцыяльна падтрымваным функцыянам толькі ў версіі 16.3 у марцы 2018 года, праз майже тры гады. У першыя часы распространення Redux хранар дзейсна быў практычным спосабам зробіць значэнне доступным у будзь-якай частыне структуры, не праходзячы яго чераз кожны компонент.

    Гэта перестало быць правдай, калі Context стаў стабільным, але навчанне так і не адпрацавала гэты момент. У навучальных матэрыялах продовжвалі викорыстоўваць метод prop drilling як прычыну проблемы, таму што яго лёгка показаць на дошцы, і гэты звычак застаўся дужо пасля таго, як прычына яго з’явілася зникла.

    Адзінакавая праця з дадзенням — гэта радыяванне, а не змена. Наступны вопыт: што выходзіць, калі радыяванае значэнне пачынае зменяцца на кліенте.

    Змена стану — гэта самэўсё, што робіць useState

    Возьмем прыклад пераключэння тэмы: адна зменная, якая храніць light або dark, якая переключаецца за дапамою кнопкі, распакованай пры самым тэксте, який яе паказвае.

    // components/SettingsPanel.jsx
    import { useState } from 'react';
    
    function SettingsPanel() {
      const [theme, setTheme] = useState('light');
      return (
        <div>
          <p>Current theme: {theme}</p>
          <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
            Toggle Theme
          </button>
        </div>
      );
    }
    export default SettingsPanel;
    

    Пры натыканні на кнопку вызываецца setTheme, SettingsPanel перарысовываецца з новым значэнням, і пасловік апошньваецца. Не трэба жадных бібліятэкаў, і ўсё можа здзейсніцца без яных. Контроль над значэнням, його змена і перарысовыванне таго, хто яго контролюе, — гэта самэўсё, для чаго існуе useState, і кожны React-дзял працуе так адноўсільна.

    Зазвычай прыклад паказуецца з якім-небудзь пераборам, який выкаанаў усю рэальную работу. Складнасць не ў тым, што зменяецца значэнне; складнасць у тым, што значэнне чытаецца ў іншым месцы. Уявіце сабе, што кантактная плячо застаецца ўнутрь SettingsPanel.jsx, тады як Header.jsx і Sidebar.jsx, два незв’язаныя файлы, якія ніхто не імпортуе і якія самі не імпортуюцца SettingsPanel, таксама должны паказваць чырговую тэму. Стан, створанный за дапамою useState, належыць адзінам экземпляру компонента. Нічога за межамі гэтага компонента не можа яго прачытаць або быць паведамленам пра змяны, як толькі спакульная бацька не перадасе яго далей, што знову прыводзіць да проблемы з передачай параметраў.

    Такім чынам useState ідеальна справляецца з пераменамі. Адкрытым пытаннем застаецца тое, як два компоненты без спакульнай бацькі могу автаматычна аднойчы перавыкацца без параметраў.

    Аднароджэнне стану між незв’язанымі компонентамі за дапамою useSyncExternalStore

    Якщо ні Header, ні Sidebar не можу прымкнуць гэтую значэнне, яго трэба зберагчы за межамі дрэва компанентоў: у зменнай на рэвэлі модуля, якая ініцыязаванае адно раз, калі ўпершыню імпортуецца яго файл, а не ўнутры якой-небудзь функцыі компанента. React ніколі не бачыць, калі звычная зменна перазначаецца, таму ён не можа самастоятельна перыскалаць вывод, калі гэта значэнне зменяецца. Невеликі модуль-хранальнік заполняе гэты прыемак.

    // store/themeStore.js
    import { useSyncExternalStore } from 'react';
    
    let state = { theme: 'light' };
    const listeners = new Set();
    export function setTheme(theme) {
      state = { ...state, theme };
      listeners.forEach((listener) => listener());
    }
    function subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    }
    export function useTheme() {
      return useSyncExternalStore(subscribe, () => state.theme);
    }
    

    Модуль мае об’ект state і множыну Set з абонентамі. Функцыя setTheme заменяе state на новы об’ект і вызывае кожнага з абонентамі. Прыватная функцыя рэгістрацыі даджае абонента ў гэтую множыну і вяртае функцыю для вычысцэння, якая яго адключае. Функцыя useTheme спаявае все гэта з React за дапамою useSyncExternalStore — хука, спецыяльна створанага для стану, які знаходзится за межамі компонентаў, але чытваецца калькамі з іх.

    Хук прымеў тут два аргументы. Першы, тая функцыя рэгістрацыі, паведамляе React, як прыўязаць функцыю-вызов, якая павінна выконвацца ўсё час, калі зменяецца значэнне званаравана, і яна павінна вяртаць адповідную функцыю для адключэння для вычысцэння. Другі, getSnapshot, тут функцыя-стрэлка () => state.theme, — гэта спосаб, яким React запрашоўвае текущае значэнне, калі гэта неабходна.

    Двое корыстувацоў імпортуюць хук, і ніхто з іх не ведае пра іншага.

    // components/Header.jsx
    import { useTheme } from '../store/themeStore';
    
    function Header() {
      const theme = useTheme();
      return <header className={theme === 'dark' ? 'header-dark' : 'header-light'}>Site Header</header>;
    }
    export default Header;
    
    // components/Sidebar.jsx
    import { useTheme } from '../store/themeStore';
    
    function Sidebar() {
      const theme = useTheme();
      return <aside className={theme === 'dark' ? 'sidebar-dark' : 'sidebar-light'}>Navigation</aside>;
    }
    export default Sidebar;
    

    Трэці компонент выканае кнопку і імпортуе як хук, так і setTheme з таго ж сховішча.

    // components/ThemeToggleButton.jsx
    import { setTheme, useTheme } from '../store/themeStore';
    
    function ThemeToggleButton() {
      const theme = useTheme();
      return (
        <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
          Toggle Theme
        </button>
      );
    }
    export default ThemeToggleButton;
    

    Што адбываецца, пасляўказова

    Кожны наступны крок можна пазіраць, якщо запусціць код:

    • Пад час стварэння, Header і Sidebar кожны вызываюць useTheme(). React вызывае getSnapshot для обох, атрыбуе значэнне 'light' і рендаруе іх з яго.
    • React таксама вызывае функцыю рэгістрацыі адна раз на кожны компонент, таму два внутрашняяя функцыі-вызывы React дадаюцца да спяльнага набора listeners. Компоненты застаюцца незалежнымі элементамі ў гэтым наборе.
  • Клік у ThemeToggleButton вызывае setTheme('dark'). Спачатку значэнне state перазначаецца на абсалютна новы об’ект { theme: 'dark' }; гэта звычны JavaScript, без жадных спецыяльнасцей для React.
  • Далее setTheme пераглядае весь набор і запускае кожны слухач. Паколькі гэтыя слухачы належаць React, ўсуненне іх запускае React да таго, каб ён пераглядаў усі зарэгістраваныя компоненты.
  • React зноў вызывае getSnapshot для Header, бачыць 'dark' замест 'light' і перарысоввае яго. Той жа чаканне прыводзіць да перарысоввання Sidebar.
  • Гэты setTheme не ёсць функцыяй-зменнікам, якая вяртаецца з useState. Гэта звычны рукапісны код, які выпаловае два заведамення ў аднам вызыве: апдэйтуе істочнік правды, а потым паведамляе тых, хто слухае.

    Ніякіх параметраў не было перадана нікуды. Весь схематызм складаецца з імпортаў. Гэта, прыблізна, тое, што Zustand рабіць унутршняя: маленькая, запакаваная версія таго ж шаблону регистрацыі і паведамлення.

    Важныя моманты, якія варта знать

    • getSnapshot павінен вяртаць тое ж значэнне, калі нічы не змянілася. Безпечнае ўжыцтво прымітива, такога як state.theme; стварэнне новага об’екта або масіву з кожным вызывам прымусвае React думаць, што хранальнік постоянна зміняецца.
    • Якщо вы відрасавляеце на серверы, useSyncExternalStore прыймае трэці аргумент, getServerSnapshot, для пачатковага HTML. Стан на рэвэлі модуля таксама дзеліцца межы запитамі, таму не трэба кластыць данні конкрэтных корыстувачаў у яго.

    Чаму Context не ёсць безпечным прыемнікам

    У разе выкарыстоўвання магазіна модуляў няма чога аддаць. state у файле themeStore.js ніколі не знаходзіцца ў дрэве компонентаў; компоненты апыходзяць да яго через вызов хука, без участі абяцеллера-прадку. Абгортаванне App у ThemeProvider дадае ўзлок, які нічога не перадае.

    Кантэкст таксама мае своія правядзімыя застосаванні: обмежэнне значэння ў адны паддрэво чы і замена залежнасці ў тэстах праз адрасаванне іншага абяцеллера. Хоць часта яго рэкамендуюць як абераглівы альтэрнатыўны варыянт бібліятэкі, у гэтым ролі ўсё жа яго выкарыстоўванне коштае больш, чым здаецца. Разглядзіце можна адны кантэкст, які храніць як тэму, так і кошык.

    // context/AppContext.jsx
    import { createContext, useState } from 'react';
    
    const AppContext = createContext();
    export function AppProvider({ children }) {
      const [state, setState] = useState({
        theme: 'light',
        cart: ['book'],
      });
      return <AppContext.Provider value={state}>{children}</AppContext.Provider>;
    }
    export default AppContext;
    

    Маленькая етыкетка выводзіць толькі інфармацыю пра тэму.

    // components/ThemeLabel.jsx
    import { useContext } from 'react';
    import AppContext from '../context/AppContext';
    
    function ThemeLabel() {
      const { theme } = useContext(AppContext);
      return <span>{theme}</span>;
    }
    
    export default ThemeLabel;
    

    useContext(AppContext) прыягае ThemeLabel да всю аб’ект, які трывае Provider, а не толькі да theme. Калі cart змянюецца ў іншым месцы, Provider атрымлее новы аб’ект state, і чырвоняючыся на тое, што рэферэнс разны, ThemeLabel таксама перзначаецца, нават якщо theme не змяніўся і компонент ніколі не чыпае cart. У контэксте няма вбудованага спосабу сказаць "пракінуць мяне толькі калі змяніцца гэты поле"; кожны корыстувач пракінаецца пасля кожной змены значэння.

    Магазін з пакета, прыказанага ў пярэднім разделе, вядома абходзіць гэта проблема. Функцыя useTheme() чытае толькі адзін фрагмент дадзеных через getSnapshot, і компонент перарысваецца толькі тады, калі значэнне, яке вяртаецца з гэтага фрагмента, зменшыцца. Іспользованне Context як проміжнага рашэння дапамагае захаваць месца на серверы, але прыводзіць да большага колькасці перарысваў. Можна зменшыць гэта, раздзеліўшы стан на калькі меньшых контэкстаў, але тады вы самі ствараеце тое, што магазін на адной основе селектара дае вам безкоштовна.

    Дзе бібліятэкі стану дасправа здобываюць своё месца

    Нічыя з гэтых прычын не робіць Redux, Zustand чыста MobX беспраганнай. Чатыро падстав для іх викорыстоўвання зазналі няудачы; самыя бібліятэкі — ні. Проблему передачы даных праз роўнейшыя слойвы можна рашыць за дапамогою Context. Змену стану — за дапамогою useState. Автаматычныя апдэйты ў некаляжных компонентах — за дапамогою useSyncExternalStore, які ўключаецца ў React. А Context, які выдавался компромісам, на самай працоўцы выявляецца дорожэ, чым маленькая бібліятэка для спакульнаваных, часта змінююцыхся значэнняў.

    Практычная патрэба ў бібліятэкі выступае тады, калі спакульны стан мае багато незалежных змінювальнікаў, а не пераважна чытачаў, і калі колькасць компонентаў, якія яго викорыстоўваюць, перавышае тое, што можа падтрымаць у ўзгоднасці калькі мануальна створаных модульных сховішчаў. Коордынацыя апдэйтаў, мідлварк, інструменты разработчика і прагнозаваная відлік змян у такых масштабах — гэта там, дзе зрелая бібліятэка прадказуе сабе адпаведную цэну, і яна заслуговае на адзінэльны разбор.

    Ключовыя выводы

    • Калікуюце Context, а не магазын, калі ўдзельныя проблемы — это перадача значэнняў через слоі, якія іх не выкарыстоўваюць.
    • useState — гэты правы інструмент для змены стану, які належыць аднаму компоненту.
    • Для стану, які дзеліцца межаў компонентаў без спяльнага родзіча, часта дапамагае невялікі магазын, створаны на базе useSyncExternalStore.
    • Адны шырокі Context перарэндруе кожнага корыстувальця праз кожную змену; для часта зменяючыхся дадзэнняў лепш выкарыстоўваць вузкія контэкты або магазын на базе селектараў.
    • Адмацавайце бібліятэку стану, калі у вас ёсць многа незалежных прадаўцаў дадзэнняў і зростаючыя групы чытачаў, а не чераз проблему “prop drilling”.

    Супаўзяныя матэрыялы

  • Чаму push() заставляе UI-інтерфейс React стацыяным і як новыя рэферэнсы гэта лечаць — З'ясавайце, чаму мутацыі массиваў у useState ніколі не спрыяюць павтарнаму атрыбутаванню, як працюе пераканальванне рэферэнсаў у React, і калі варытаць выкарыстоўваць копіі з распрасам і функцыональныя апдэйты.