Контекст

Контекст нь өгөгдлүүдийг пропс ашиглан компонентын бүх түвшингүүдээр доош дамжуулахгүйгээр шууд компонентын мод ашиглан дамжуулах боломжийг олгодог.

Энгийн React програмд өгөгдлийг дээрээс доош буюу (эцэг компонентоос хүү компонент руу) гэсэн чиглэлтэйгээр пропсуудыг дамжуулдаг. Гэхдээ зарим (locale preference, UI theme) програмд олон компонентуудад дуудагдан ашиглагддаг пропсуудын хувьд энэ арга нь тохиромжгүй юм. Тиймээс контекст ашиглан ийм төрлийн өгөгдлүүдийг шууд компонент модоор компонентууд хооронд дамжуулж болно.

Хэзээ контекстыг ашиглах

Контекст нь “global” түвшинд хамаарагдах өгөгдлүүдийг React компонентын модноос шууд авч ашиглах боломжтой болгодог. Тухайлбал current authenticated user, theme болон preferred language гэсэн өгөгдлүүд үүнд хамаарагдана. Жишээ нь доорх код Button компонентыг хэлбэржүүлэхийн тулд “theme” пропсыг дамжуулж өгсөн байна.

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // The Toolbar component must take an extra "theme" prop
  // and pass it to the ThemedButton. This can become painful
  // if every single button in the app needs to know the theme
  // because it would have to be passed through all components.
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}

Контекстыг хэрэглэснээр бид дундын элементүүдээр пропсуудыг дамжуулах шаардлагагүй болно.

// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // Use a Provider to pass the current theme to the tree below.
    // Any component can read it, no matter how deep it is.
    // In this example, we're passing "dark" as the current value.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // Assign a contextType to read the current theme context.
  // React will find the closest theme Provider above and use its value.
  // In this example, the current theme is "dark".
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

Контекстыг ашиглахаас өмнө

Контекст нь компонентын модны өөр өөр түвшинд байгаа компонентууд тухайн өгөгдлийг ашиглах шаардлагатай болох үед ихэвчлэн хэрэглэгддэг. Гэхдээ үүнийг аль болох бага ашиглах хэрэгтэй, учир нь энэ компонентыг дахин ашиглахад илүү түвэгтэй болгодог.

Хэрвээ та пропсуудыг зөвхөн олон түвшингүүдээр дамжуулахаас зайлс хийх байгаа бол, component composition нь контекстоос илүү хялбар шийдэл юм.

Жишээ нь Page компонент нь user болон avatarSize гэсэн пропсуудыг доод түвшин рүү дамжуулсанаар Link болон Avatar гэсэн бүр доод түвшний компонентууд эдгээр пропсуудыг авч ашиглах боломжтой болж байна.

<Page user={user} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... which renders ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... which renders ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

Хэрвээ зөвхөн хамгийн сүүлийн Avatar компонент л эдгээр user болон avatarSize пропсуудыг ашиглах шаардлагатай байсан бол дунд талын түвшингүүдээр дамжуулах нь илүү үйлдэл биш гэж үү. Хэрвээ Avatar компонент нь дахин өөр пропсуудыг хамгийн дээд түвшний компонентоос авч ашиглах шаардлагатай болвол тэдгээр дунд нь байгаа бүх компонентуудад дахин нэмэх шаардлагатай болно.

Контекст ашиглахгүй энэ асуудлыг шийдэх нэг арга нь Avatar компонентыг өөрийг нь шууд дамжуулах юм. Ингэснээр дундын компонентууд user болон avatarSize пропсуудын талаар мэдэх шаардлагагүй болно.

function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// Now, we have:
<Page user={user} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout userLink={...} />
// ... which renders ...
<NavigationBar userLink={...} />
// ... which renders ...
{props.userLink}

Ингэж өөрчилсөнөөр зөвхөн хамгийн дээд түвшний Page компонент л зөвхөн Link болон Avatar компонентууд user болон avatarSize пропсуудыг хэрхэн ашиглаж байгааг мэдэх юм.

Энэ inversion of control загварыг ашигласнаар таны програмд дамжуулагдах пропсуудын хэмжээ багасч үр дүнд нь код тань илүү цэгцтэй болох болно. Мөн үндсэн (root) компонентуудад илүү эрх мэдлийг өгөх болно. Гэхдээ энэ арга нь эцсийн зөв шийдэл биш бөгөөд бүх тохиолдлуудад зөв ажиллахгүй, дээд түвшин рүү хэт их зүйлсийг төвлөрүүлсэнээр дээд түвшний компонентуудыг илүү түвэгтэй, ойлгоход хэцүү болгоно, улмаар доод түвшний компонентуудыг үүндээ нийцүүлэн илүү уян хатан байхыг шаардана.

Компонент нь зөвхөн нэг хүү компонентоор хязгаарлагдахгүй. Та олон хүү компонентуудыг дамжуулах боломжтой, мөн цаашлаад олон хүү компонентуудыг дамжуулах боломжтой “slots” уудыг дамжуулж болно. Эндээс харах

function Page(props) {
  const user = props.user;
  const content = <Feed user={user} />;
  const topBar = (
    <NavigationBar>
      <Link href={user.permalink}>
        <Avatar user={user} size={props.avatarSize} />
      </Link>
    </NavigationBar>
  );
  return (
    <PageLayout
      topBar={topBar}
      content={content}
    />
  );
}

Энэ загвар (pattern) нь хүү компонентуудыг тэдгээрийг шууд агуулсан эцэг компонентуудаас (immediate parents) салган ашиглах шаардлагатай байгаа бүх тохиолдлуудад ашиглаж болно. Бүр цаашлаад хэрвээ хүү компонент нь рендер хийгдэхээсээ өмнө эцэг компоненттой харилцах шаардлагатай болвол render props той хамт ашиглагдаж болно.

Заримдаа нэг ижил өгөгдлүүд рүү компонентын модны өөр өөр түвшинд байгаа компонентуудаас хандах шаардлагатай болдог. Контекст нь эдгээр өгөгдлүүдийг доод түвшний компонентуудад түгээх (“broadcast”) эсвэл өөрчлөх боломжийг олгодог. Зарим түгээмэл жишээнүүдэд контекст ашигласан нь “managing the current locale, them, эсвэл a data cache” гэсэн бусад аргуудыг ашигласнаас илүү хялбар болдог.

API

React.createContext

const MyContext = React.createContext(defaultValue);

Контекстын обьектыг үүсгэх. React нь тухайн контекстын обьектыг ашиглахдаа компонентын модны өөрөөс нь дээш хамгийн ойр орших Provider оос өгөгдлүүдээ авдаг.

defaultValue аргумент нь зөвхөн компонентын модонд тухайн компонентод өөрөөс нь дээш байрлах Provider олдохгүй байх тохиолдолд ашиглагддаг. Мөн энэ аргумент нь тухайн компонентыг өөр компонентод агуулагдалгүй (without wrapping) хязгаарлагдмал байдлаар тест хийхэд илүү тохиромжтой.

Context.Provider

<MyContext.Provider value={/* some value */}>

Бүх контекстын обьект нь React Provider компоненттой холбогдсон байдаг, ба энэ нь тухайн контекстыг ашиглаж байгаа компонентуудад тухайн контекстэд өөрчлөлт хийх боломжийг олгодог.

value пропс нь Provider ийн хүү компонентууд (хэрэглэгч компонентууд) руу дамжуулагддаг. Нэг Provider нь олон хэрэглэгч компоненттэй холбогдож болно. Provider ууд нь нэг нэгэндээ агуулагдаж компонентын модонд дамжуулагдаж буй утгуудыг (values) даран тодорхойлж (override) болно.

Тухайн Provider ийн бүх хэрэглэгч компонентууд нь тухайн Provider ийн value пропс д өөрчлөлт орох бүрд дахин рендэр хийгддэг. Provider аас хэрэглэгч компонентуудыг рендэр хийж байгаа нь shouldComponentUpdate функцтай холбоогүй, улмаар эцэг компонент нь өөрчлөлт хийхээ зогсоосон ч хэрэглэгч компонентууд нь өөрчлөгдөх болно.

Өөрчлөлт нь шинэ болон хуучин утгуудыг ижил алгоримтаар Object.is харцуулсанаар тодорхойлогддог.

Тэмдэглэл

Энэ арга нь value пропсыг обьект хэлбэрээр дамжуулах үед зарим нэг асуудал үүсч байгаа. Анхааруулга.

Class.contextType

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* perform a side-effect at mount using the value of MyContext */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* render something based on the value of MyContext */
  }
}
MyClass.contextType = MyContext;

Класс дахь contextType проперти нь React.createContext() аас үүссэн контекстын обьектоор утга олгогдож байна. Энэ нь хамгийн ойр орших тухайн контекстын утгыг this.context ийг ашиглаж авч байна. Энэ this.context ийг lifecycle функцууд болон рендер функцад мөн ашиглаж болно.

Тэмдэглэл:

Та энэ API ийг ашигласнаар зөвхөн single контекстыг зөвшөөрөх боломжтой. Дэлгэрэнгүй мэдээлэл эндээс харна уу Consuming Multiple Contexts.

Хэрвээ та туршилтын public class fields syntax ийг ашиглаж байгаа бол, мөн static class field ийг ашиглан өөрийн contextType ийг зарлах боломжтой.

class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* render something based on the value */
  }
}

Context.Consumer

<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>

React компонент нь контекстын өөрлчлөлтийг дэмждэг. Энэ нь контекстыг функцианаль компонент (functional component) дотор ашиглах боломжтой болгосон.

Функцыг хүү компонентоор ашиглах нь function as a child. Функц нь тухайн контекстын утгыг хүлээн аваад React node буцаадаг. Функцэд дамжуулагдаж байгаа value аргумент нь компонентын модонд байх тухайн контекстээс дээш хамгийн ойр байрлах Provider ийн value пропстой тэнцүү байна. Хэрвээ тухайн контекстээс дээш орших ямар ч Provider олдохгүй бол value аргумент нь createContext() функцээр дамжуулагдсан defaultValue тай тэнцүү байна.

Тэмдэглэл

Функцыг хүү компонентоор ашиглах загварыг (pattern) эндээс харах render props.

Context.displayName

Контекст нь displayName гэх стринг тѳрѳлтэй утга авах нэгжтэй. React DevTools нь энэ стрингийг ашиглан дэлгэцэнд юу харуулахаа шийддэг.

Жишээ нь, доорхи компонент нь MyDisplayName гэж DevTools дээр харагдана:

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools

Жишээнүүд

Динамик контекст

Динамик утгуудыг theme д ашигласан илүү түвэгтэй жишээ:

theme-context.js

export const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
};

export const ThemeContext = React.createContext(
  themes.dark // default value
);

themed-button.js

import {ThemeContext} from './theme-context';

class ThemedButton extends React.Component {
  render() {
    let props = this.props;
    let theme = this.context;
    return (
      <button
        {...props}
        style={{backgroundColor: theme.background}}
      />
    );
  }
}
ThemedButton.contextType = ThemeContext;

export default ThemedButton;

app.js

import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';

// An intermediate component that uses the ThemedButton
function Toolbar(props) {
  return (
    <ThemedButton onClick={props.changeTheme}>
      Change Theme
    </ThemedButton>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: themes.light,
    };

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };
  }

  render() {
    // The ThemedButton button inside the ThemeProvider
    // uses the theme from state while the one outside uses
    // the default dark theme
    return (
      <Page>
        <ThemeContext.Provider value={this.state.theme}>
          <Toolbar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
        <Section>
          <ThemedButton />
        </Section>
      </Page>
    );
  }
}

ReactDOM.render(<App />, document.root);

Агуулагдсан (nested) компонентоос контекстыг өөрчлөх

Компонентын модны аль нэг түвшинд байрлах дурын компонентоос контекстэд өөрчлөлт хийх шаардлага их гардаг. Энэ нөхцөлд функцыг контекстээр доош дамжуулан хэрэглэгч компонентуудаас контекстыг өөрчилдөг:

theme-context.js

// Make sure the shape of the default value passed to
// createContext matches the shape that the consumers expect!
export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});

theme-toggler-button.js

import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
  // The Theme Toggler Button receives not only the theme
  // but also a toggleTheme function from the context
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

export default ThemeTogglerButton;

app.js

import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };

    // State also contains the updater function so it will
    // be passed down into the context provider
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    // The entire state is passed to the provider
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}

function Content() {
  return (
    <div>
      <ThemeTogglerButton />
    </div>
  );
}

ReactDOM.render(<App />, document.root);

Олон контекст хэрэглэх

Контекстыг хурдан дахин рендэр хийдэг байлгахын тулд React нь бүх контекстын хэрэглэгчдийг компонентын модны салангид node нүүд дээр байрлуулсан байх хэрэгтэй.

// Theme context, default to light theme
const ThemeContext = React.createContext('light');

// Signed-in user context
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    // App component that provides initial context values
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}

// A component may consume multiple contexts
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

Хэрвээ хоёр болон түүнээс дээш контекстууд ихэвчлэн хамтдаа хэрэглэгдэж байвал тухайн контекстуудыг хангах өөр өөрийн рендэр пропс компонентыг үүсгэх нь зөв юм.

Сануулга

Контекст нь reference identity ийг ашиглан компонентыг дахин рендэр хийхээ шийдэж байгаа учраас эцэг компонент дахин рендэр хийгдэх үед хэрэглэгч компонентуудад шаардлагагүй рендэр хийгдэх тохиолдлууд гарч болно. Жишээ нь доорх код нь Provider ийг дахин рендэр хийх бүрд бүх хэрэглэгч компонентууд дахин рендэр хийгдэх болно, учир нь value дотор үргэлж шинэ обьектууд үүсэж байдаг.

class App extends React.Component {
  render() {
    return (
      <Provider value={{something: 'something'}}>
        <Toolbar />
      </Provider>
    );
  }
}

Үүнийг шийдэхийн тулд утгуудыг (value) эцэг компонентын state руу зөөх хэрэгтэй.

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }

  render() {
    return (
      <Provider value={this.state.value}>
        <Toolbar />
      </Provider>
    );
  }
}

Хуучин API

Тэмдэглэл

React шинэ контекст API руу шилжсэн. Хуучин API нь React 16.x хувилбаруудад дэмжигдэж байгаа. Гэхдээ энэ хуучин API ийг ашиглаж байгаа програмууд шинэ хувилбар руу шилжих хэрэгтэй. Хуучин API нь ирээдүйд React ийн хувилбаруудаас хасагдах болно. Хуучин API ийн талаар унших