Skip to content

State Management

State Management چیست؟

از نظر فنی، هر کامپوننت Vue به طور خودکار state واکنش‌‌پذیری خود را "مدیریت" می‌کند. به عنوان مثال یک کامپوننت ساده شمارنده را در نظر بگیرید:

vue
<script setup>
import { ref } from 'vue'

// state
const count = ref(0)

// actions
function increment() {
  count.value++
}
</script>

<!-- view -->
<template>{{ count }}</template>
vue
<script>
export default {
  // state
  data() {
    return {
      count: 0
    }
  },
  // actions
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

<!-- view -->
<template>{{ count }}</template>

این یک واحد مجزا با اجزای زیر است:

  • state، داده‌ای که برنامه ما بر اساس آن هدایت می‌شود.
  • view، پیاده‌سازی ظاهری از state.
  • actions، راه‌های احتمالی برای تغییر state در واکنش به ورودی‌های کاربر از view.

این یک ارائه ساده‌ای از مفهوم "جریان داده یک‌طرفه (one-way data flow)" است:

نمودار state flow

اما سادگی زمانی شروع به فروپاشی می‌کند که چندین کامپوننت وجود داشته باشند که state مشترکی داشته باشند:

  1. چندین view ممکن است به یک قسمت از state وابسته باشند.
  2. action های مختلف view ها ممکن است نیاز به تغییر یک قسمت مشترک از state داشته باشند.

برای مورد اول، یک راه حل ممکن این است که state مشترک را به یک کامپوننت پدر مشترک انتقال دهیم و سپس آن را به عنوان props به پایین پاس دهیم. اما این عمل در درخت‌های کامپوننت با سلسله‌مراتب عمیق خسته‌کننده است و منجر به مشکل دیگری به نام Prop Drilling می‌شود.

برای مورد دوم، اغلب خود را در حال استفاده از راه حل‌هایی مانند دسترسی مستقیم به instance های والد / فرزند از طریق template refs، یا تلاش برای تغییر و همگام‌سازی چندین کپی از state از طریق رویدادهای emit شده می‌یابیم. هر دو الگو شکننده هستند و به سرعت منجر به تولید کد غیرقابل نگهداری می‌شوند.

یک راه‌حل ساده‌تر این است که state مشترک را از کامپوننت‌ها خارج کنیم و آن را بصورت یگانه و سراسری مدیریت کنیم. با این کار درخت کامپوننت‌ها ما به یک "view" بزرگ تبدیل می‌شود، و هر کامپوننتی می‌تواند به state دسترسی پیدا کند یا اکشن‌ها را فراخوانی کند، صرف‌نظر از اینکه در کجای درخت قرار دارد!

مدیریت ساده State با Reactivity API

در Options API، داده‌های reactive با استفاده از گزینه data()‎ اعلام می‌شوند. در داخل آن، آبجکت برگردانده شده توسط data()‎ با استفاده از تابع reactive()‎ واکنش‌گرا می‌شود، که به عنوان یک API عمومی نیز در دسترس است.

اگر یک state داشته باشید که باید توسط چندین نمونه به اشتراک گذاشته شود، می‌توانید از reactive()‎ برای ایجاد یک آبجکت reactive استفاده کنید، و سپس آن را در چندین کامپوننت import کنید:

js
// store.js
import { reactive } from 'vue'

export const store = reactive({
  count: 0
})
vue
<!-- ComponentA.vue -->
<script setup>
import { store } from './store.js'
</script>

<template>From A: {{ store.count }}</template>
vue
<!-- ComponentB.vue -->
<script setup>
import { store } from './store.js'
</script>

<template>From B: {{ store.count }}</template>
vue
<!-- ComponentA.vue -->
<script>
import { store } from './store.js'

export default {
  data() {
    return {
      store
    }
  }
}
</script>

<template>From A: {{ store.count }}</template>
vue
<!-- ComponentB.vue -->
<script>
import { store } from './store.js'

export default {
  data() {
    return {
      store
    }
  }
}
</script>

<template>From B: {{ store.count }}</template>

حالا هر زمانی که آبجکت store تغییر کند، هر دو <ComponentA> و <ComponentB> به‌طور خودکار view خود را به‌روزرسانی خواهند کرد - دیگر یک منبع واحد داده داریم.

با این حال، این به معنای آن است که هر کامپوننتی که store را import می‌کند، می‌تواند به هر روشی که می‌خواهد آن را تغییر دهد:

template
<template>
  <button @click="store.count++">
    From B: {{ store.count }}
  </button>
</template>

در حالی که این در موارد ساده کار می‌کند، اما state سراسری که به‌طور دلخواه توسط هر کامپوننتی قابل تغییر باشد، در بلندمدت چندان قابل نگهداری نخواهد بود. برای اطمینان از اینکه منطق تغییردهنده state مانند خود state متمرکز شده باشد، توصیه می‌شود متدهایی را با نام‌هایی که قصد آن action خاص را بیان می‌کنند، روی store تعریف کرد:

js
// store.js
import { reactive } from 'vue'

export const store = reactive({
  count: 0,
  increment() {
    this.count++
  }
})
template
<template>
  <button @click="store.increment()">
    From B: {{ store.count }}
  </button>
</template>

نکته

توجه کنید که هندلر کلیک از store.increment()‎ با پرانتز استفاده می‌کند - این برای صدا زدن متد با this ضروری است چون متد برای کامپوننت نیست.

اگرچه در اینجا از یک آبجکت واکنش‌گرای یگانه به عنوان یک store استفاده کرده‌ایم، اما می‌توانید state واکنش‌گرا ایجاد شده با سایر Reactivity APIs مانند ref()‎ یا computed()‎ و یا حتی state سراسری را از یک Composable برگردانید:

js
import { ref } from 'vue'

// سراسری - ایجاد شده در اسکوپ ماژول state
const globalCount = ref(1)

export function useCount() {
  // محلی، ایجاد شده برای هر کامپوننت state
  const localCount = ref(1)

  return {
    globalCount,
    localCount
  }
}

این موضوع که سیستم واکنش‌پذیری Vue از کامپوننت جدا شده است، آن را بسیار انعطاف‌پذیر می‌کند.

در نظر گرفتن‌ SSR

اگر در حال ساختن برنامه‌ای هستید که از Server-Side Rendering (SSR) استفاده می‌کند، الگوی بالا می‌تواند به دلیل اینکه store یک سینگلتون مشترک در میان چندین درخواست است، منجر به مشکلاتی شود. این موضوع با جزئیات بیشتری در راهنمای SSR بحث شده است.

Pinia

در حالی که راه حل مدیریت state دست‌ساز ما در سناریوهای ساده کافی است، اما در برنامه‌های تولید شده در مقیاس بزرگ نکات بیشتری وجود دارد که باید در نظر گرفته شود:

  • قوانین سخت گیرانه تر برای کار تیمی
  • ادغام با Vue DevTools شامل تایم‌لاین، بازرسی درون کامپوننت، و دیباگ بصورت time-travel
  • Hot Module Replacement
  • پشتیبانی از Server-Side Rendering

Pinia یک کتابخانه مدیریت state است که تمام موارد بالا را پیاده‌سازی می‌کند. توسط تیم اصلی Vue نگهداری می‌شود و هم با Vue 2 و Vue 3 کار می‌کند.

کاربران فعلی ممکن است با Vuex آشنا باشند، کتابخانه رسمی قبلی مدیریت state برای Vue. با اینکه Pinia نقش مشابهی در اکوسیستم دارد، Vuex حالا در حالت نگهداری قرار گرفته است. هنوز کار می‌کند، اما دیگر ویژگی‌های جدیدی دریافت نخواهد کرد. توصیه می‌شود برای برنامه‌های جدید از Pinia استفاده کنید.

Pinia ابتدا به عنوان اکتشافی در مورد اینکه نسل بعدی Vuex می‌تواند چه شکلی داشته باشد، شروع شد و بسیاری از ایده‌هایی که در بحث‌های تیم اصلی برای Vuex 5 مطرح شده بود را پیاده کرد. در نهایت، متوجه شدیم که Pinia اکثر آنچه را که می‌خواستیم در Vuex 5 داشته باشیم پیاده‌سازی کرده است، و تصمیم گرفتیم آن را توصیه کنیم.

در مقایسه با Vuex، در Pinia ما API ساده‌تری با پیچیدگی کمتری مشاهده می‌کنیم، API‌های شبه Composition-API خواهیم دید و مهم‌تر از همه اینکه هنگام استفاده با TypeScript از type inference قوی پشتیبانی می‌کند.

State Management has loaded