استخدام السياق (Context) في React
يُزوِّدنا السياق (Context) بطريقة لتمرير البيانات عبر شجرة المُكوّنات بدون الحاجة لتمرير الخاصيّات props
يدويًّا من الأعلى للأسفل في كل مستوى.
تُمرَّر البيانات في تطبيقات React الاعتيادية من المستوى الأعلى للأسفل (أي من المكوّنات الآباء إلى المكوّنات الأبناء) عبر الخاصيّات props
، ولكن قد يكون هذا بطيئًا لبعض أنواع الخاصيّات (مثل تفضيلات اللغة وقوالب واجهة المستخدم) والتي تحتاجها العديد من المكوّنات ضمن التطبيق. يُزوِّدنا السياق بطريقة لمشاركة القيم كتلك الموجودة بين المكوّنات بدون الاضطرار لتمرير الخاصيّات عبر كل مستوى من الشجرة.
متى نستخدم السياق
يكون السياق مُصمَّمًا لمشاركة البيانات التي تُعتبر عامّة (global) لشجرة مكوّنات React، مثل المستخدم قيد المصادقة حاليًّا، أو القالب، أو تفضيلات اللغة. على سبيل المثال في الشيفرة التالية نُمرِّر خاصيّة القالب (theme
) يدويًّا من أجل تنسيق مكوّن الزر Button
:
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
// يجب أن يأخذ مكون شريط الأدوات Toolbar خاصيّة theme إضافية
// ويمررها إلى ThemedButton
// قد يصبح هذا عملًا شاقًّا إن احتاج كل زر في التطبيق أن يعرف القالب theme
// لأنه يجب تمريره عبر كل المكونات
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
function ThemedButton(props) {
return <Button theme={props.theme} />;
}
نتجنب باستخدام السياق تمرير الخاصيات عبر عناصر وسيطة:
// يُتيح لنا السياق تمرير القيمة بعمق شجرة المكونات
// بدون الحاجة إلى تمريره إلى كل مكون
// أنشئ السياق لأجل القالب الحالي (مع وضع القيمة light كقيمة افتراضية)
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// استخدم Provider لتمرير القالب الحالي إلى الشجرة التالية.
// يستطيع أي مكون قراءة القالب الحالي بغض النظر عن مدى عمقه في شجرة المكونات
// في هذا المثال نمرر "dark" كقيمة للقالب الحالي
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// لا يجب على المكون الموجود في الوسط أن يمرر القالب للمستويات الأدنى منه
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton(props) {
// استخدام Consumer لقراءة سياق القالب الحالي
// ستبحث React عن أقرب Provider للقالب في المستوى الأعلى منها وتستخدم قيمته
// في هذا المثال قيمة القالب الحالي هي "dark"
return (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
);
}
قبل أن تستخدم السياق
يُستخدَم السياق بشكل أساسي عند الحاجة للوصول إلى بعض البيانات من قبل العديد من المكونات في مستويات متداخلة مختلفة، ولكن لا يجب استخدامه بكثرة لأنّ يجعل من إعادة استخدام المكونات أمرًا أكثر صعوبة.
إن أردت فقط تجنّب تمرير بعض الخاصيّات عبر العديد من المستويات، فسيكون استخدام التراكيب حلًّا أبسط من استخدام السياق.
على سبيل المثال افترض وجود مكون للصفحة Page
والذي يُمرِّر خاصيّات المستخدم user
وحجم الصورة الرمزية avatarSize
عبر مستويات عديدة للأسفل بحيث تتمكّن مكونات الرابط Link
والصورة الرمزية Avatar
الموجودة في مستويات عميقة ومتداخلة أن تقرأها:
<Page user={user} avatarSize={avatarSize} />
// ... والذي يصير ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... والذي يصير ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... والذي يصير ...
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>
قد يبدو من الفائض تمرير الخاصيّات user
و avatarSize
عبر مستويات عديدة للأسفل إن كان يحتاجه في النهاية فقط المكوّن Avatar
. من المزعج أيضًا كلّما احتاج المُكوِّن Avatar
المزيد من الخاصيّات من المستويات الأعلى فيجب عليك إضافتها في كل المستويات الوسيطة أيضًا.
من طرق حل هذه المشكلة بدون السياق هي تمرير المكون Avatar
نفسه للأسفل بحيث لا تحتاج المكوّنات الوسيطة أن تعلم حول الخاصيّة user
:
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} />
// ... والذي يصير ...
<PageLayout userLink={...} />
// ... والذي يصير ...
<NavigationBar userLink={...} />
// ... والذي يصير ...
{props.userLink}
مع هذا التغيير يحتاج فقط المكون Page
ذو المستوى الأعلى إلى أن يعرف عن استخدام المكوّنات Link
و Avatar
للمكوّنات user
و avatarSize
.
يؤدي قلب السيطرة هذا إلى جعل شيفرتك أبسط في العديد من الحالات عن طريق تقليل كمية الخاصيّات التي تحتاج تمريرها عبر تطبيقك ويُعطيك سيطرة أكبر على المكوّنات الجذريّة. على الرغم من ذلك لا يكون ذلك هو الخيار الأنسب في كل حالة، حيث أنّ نقل المزيد من التعقيد إلى مستوى أعلى في الشجرة يجعل من المكونات ذات المستوى الأعلى أكثر تعقيدًا ويجبر المكونات ذات المستويات الأدنى أن تكون مرنة أكثر مما قد ترغب.
لن تكون محدودًا بمكوّن ابن واحد، فبإمكانك تمرير مكونات أبناء متعددة أو حتى امتلاك منافذ منفصلة متعددة للأبناء كما هو موثق هنا:
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}
/>
);
}
يكفينا هذا النمط للعديد من الحالات عند الحاجة لفصل مكوّن ابن عن المكونات الآباء له. وبإمكانك أخذ هذا إلى أبعد من ذلك عن طريق خاصيّات التصيير إن كان المكوّن الابن يحتاج إلى التواصل مع المكوّن الأب قبل التصيير.
على أية حال تحتاج بعض البيانات أحيانًا أن تكون قابلة للوصول من قبل العديد من المكوّنات في الشجرة، وبمستويات متداخلة مختلفة. يُتيح لك السياق نشر مثل هذه البيانات وتغييراتها إلى جميع المكوّنات في المستويات الأدنى. تتضمّن الأمثلة الشائعة التي يكون فيها استخدام السياق أبسط من البدائل هي إدارة اللغة الحالية، أو القالب، أو مخبأ البيانات (cache).
واجهة برمجة التطبيقات (API)
React.createContext
const {Provider, Consumer} = React.createContext(defaultValue);
يُنشِئ الزوج { Provider, Consumer }
.
عند تصيير React للسياق Consumer
فستقرأ قيمة السياق الحالية من أقرب مُزوِّد Provider
فوقها في الشجرة.
يُستخدَم الوسيط defaultValue
عن طريق المستهلك Consumer
فقط عندما لا يجد مزوّد Provider
مُطابِق فوقه في الشجرة. يُفيد هذا من أجل اختبار المُكوّنات على انفراد بدون تغليفها.
ملاحظة: لا يُؤدّي تمرير القيمة undefined
كقيمة للمُزوِّد إلى استخدام المستهلك Consumer
للقيمة defaultValue
.
المزود Provider
<Provider value={/* قيمة ما */}>
وهو مُكوِّن React الذي يسمح للمستهلك Consumer
بأن يُشارك في تغييرات السياق.
يقبل خاصيّة للقيمة value
لتمريرها إلى المستهلكات المنحدرة عن هذا المُزوِّد. يُمكِن وصل مُزوِّد واحد مع العديد من المستهلكات. يُمكِن مداخلة المُزوِّدات لتجاوز قيم عميقة ضمن شجرة المُكوِّنات.
المستهلك Consumer
<Consumer>
{value => /* تصيير شيء ما اعتمادًا على قيمة السياق */}
</Consumer>
وهو مُكوِّن React الذي يُشارِك بتغييرات السياق.
يتطلّب دالة كابن له. تستقبل الدالة قيمة السياق الحاليّة وتُعيد عقدة React. تكون قيمة الوسيط value
المُمرَّرة إلى الدالة مساوية للخاصيّة value
لأقرب مُزوِّد لهذا السياق في المستويات الأعلى من الشجرة. إن لم يُوجَد مُزوِّد لهذا السياق في المستوى الأعلى، ستكون قيمة الوسيط value
مساوية للقيمة defaultValue
التي مرّرناها للتابع createContext()
.
ملاحظة: للمزيد من المعلومات حول النمط "دالة كابن" انظر إلى صفحة خاصيّة التصيير.
كل المستهلكات Consumers
المنحدرة عن المُزوِّد ستُعيد التصيير عندما تتغير قيمة الخاصيّة value
للمُزوّد. لا يخضع الانتشار من المُزوّد إلى المستهلكات المنحدرة عنه إلى التابع shouldComponentUpdate
، لذا يُحدَّث المستهلك حتى ولو كان المكوّن الأب غير خاضع للتحديث.
تُحدَّد التغييرات عن طريق مقارنة القيم الجديدة والقديمة باستخدام نفس الخوارزمية مثل Object.is
.
ملاحظة: قد تُسبِّب طريقة التغييرات المُحدَّدة بعض المشاكل عند تمرير الكائنات في الوسيط value
: سنتحدّث عن المزيد في قسم المحاذير.
أمثلة
السياق الديناميكي
theme-context.js
export const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
};
export const ThemeContext = React.createContext(
themes.dark // القيمة الافتراضية
);
themed-button.js
import {ThemeContext} from './theme-context';
function ThemedButton(props) {
return (
<ThemeContext.Consumer>
{theme => (
<button
{...props}
style={{backgroundColor: theme.background}}
/>
)}
</ThemeContext.Consumer>
);
}
export default ThemedButton;
app.js
import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';
// مكون وسيط يستخدم ThemedButton
function Toolbar(props) {
return (
<ThemedButton onClick={props.changeTheme}>
تغيير القالب
</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() {
// يستخدم الزر ThemedButton بداخل المزود ThemeProvider
// القالب من الحالة بينما يستخدم الموجود بالخارج القالب الافتراضي dark
return (
<Page>
<ThemeContext.Provider value={this.state.theme}>
<Toolbar changeTheme={this.toggleTheme} />
</ThemeContext.Provider>
<Section>
<ThemedButton />
</Section>
</Page>
);
}
}
ReactDOM.render(<App />, document.root);
تحديث السياق من المكونات المتداخلة
من الضروري أحيانًا تحديث السياق من المكون المتداخل بعمق في مكانٍ ما من شجرة المكونات. في هذه الحالة تستطيع تمرير دالة إلى الأسفل عبر السياق للسماح للمستهلكات بتحديث السياق:
theme-context.js
// التأكد من أنّ القيمة الافتراضية الممررة
// إلى createContext تطابق الشكل الذي يتوقعه المستخدم
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
theme-toggler-button.js
import {ThemeContext} from './theme-context';
function ThemeTogglerButton() {
// لا يستقبل زر تغيير القالب قيمة القالب فقط وإنما الدالة toggleTheme من السياق أيضا
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
إطفاء أو تشغيل القالب
</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,
}));
};
// تحتوي الحالة أيضًا على دالة التحديث بحيث تمرر
// إلى الأسفل إلى مزود السياق
this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme,
};
}
render() {
// تمرر كامل الحالة إلى المزود
return (
<ThemeContext.Provider value={this.state}>
<Content />
</ThemeContext.Provider>
);
}
}
function Content() {
return (
<div>
<ThemeTogglerButton />
</div>
);
}
ReactDOM.render(<App />, document.root);
استهلاك سياقات متعددة
لإبقاء قدرة السياق على إعادة التصيير بشكل سريع، تحتاج React إلى جعل كل مستهلك سياق على شكل عقدة منفصل في الشجرة:
// سياق القالب، بشكل افتراضي قيمته light
const ThemeContext = React.createContext('light');
// سياق المستخدم قيد تسجيل الدخول
const UserContext = React.createContext({
name: 'Guest',
});
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
// مكون التطبيق الذي يزودنا بقيم مبدئية للسياق
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}
// قد يستهلك المكون العديد من السياقات
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}