ثبت سفارش و انتقال داده‌ها بین صفحات

22 بهمن 1399
ثبت سفارش و انتقال داده ها بین صفحات

ثبت سفارش و انتقال داده ها بین صفحات

در قسمت قبل توانستیم کامپوننت ContactData را ایجاد کرده و آن را در صفحه بارگذاری کنیم. حالا نوبت این است که با کلیک روی <Button> سفارش کاربر را ثبت کنیم:

<Button btnType="Success">ORDER</Button>

برای انجام این کار وارد فایل ContactData.js شده و یک متد به نام orderHandler ایجاد می کنیم:

    orderHandler = () => {
        
    }

سپس آن را به دکمه ی ثبت سفارش خودمان اضافه می کنیم:

<form>
    <input className={classes.Input} type="text" name="name" placeholder="Your Name" />
    <input className={classes.Input} type="email" name="email" placeholder="Your Mail" />
    <input className={classes.Input} type="text" name="street" placeholder="Street" />
    <input className={classes.Input} type="text" name="postal" placeholder="Postal Code" />
    <Button btnType="Success" clicked={this.orderHandler}>ORDER</Button>
</form>

حالا باید orderHandler را کدنویسی کنیم تا با کلیک روی دکمه، سفارش ما در پایگاه داده ثبت شود اما مشکل اینجاست که برای انجام چنین کاری هم نیاز به ingredients داریم و هم اطلاعات تماس کاربر را می خواهیم. همانطور که می بینید انجام این کار ساده نیست و برای انجام آن باید از یک حقه استفاده کنیم!

اگر یادتان باشد کامپوننت ContactData به شکل زیر درون فایل Checkout.js بارگذاری شده است:

return (
    <div>
        <CheckoutSummary
            ingredients={this.state.ingredients}
            checkoutCancelled={this.checkoutCancelledHandler}
            checkoutContinued={this.checkoutContinuedHandler} />
        <Route
            path={this.props.match.path + '/contact-data'}
            component={ContactData} />
    </div>
); 

و حالا می خواهیم ingredients را به نوعی به همراه ContactData ارسال کنیم. برای این کار به جای استفاده از component از render استفاده می کنیم:

<Route
    path={this.props.match.path + '/contact-data'}
    render={() => (<ContactData />)} />

همانطور که می دانید انجام این کار با استفاده از component هیچ تفاوتی ندارد اما به این دلیل که آن را به صورت دستی پاس می دهیم، می توانیم prop های خودمان را به آن پاس بدهیم!

<Route
    path={this.props.match.path + '/contact-data'}
    render={() => (<ContactData ingredients={this.state.ingredients} />)} />

حالا برای تست کردن برنامه درون متد orderHandler مقدار ingredients را console.log می کنیم:

    orderHandler = (event) => {
        event.preventDefault();
        console.log(this.props.ingredients);
    }

من شیء event را دریافت و آن را با preventDefault غیرفعال کردم. اگر این کد را اضافه نکنید نمی توانید ingredients را ببینید چرا که رفتار پیش فرض فرم ها ثبت درخواست و refresh شدن صفحه است. با refresh شدن صفحه نیز قسمت console را از دست می دهیم. حالا نتیجه به راحتی در مرورگر قابل مشاهده است:

دریافت ingredients به صورت prop در قسمت console مرورگر
دریافت ingredients به صورت prop در قسمت console مرورگر

در مرحله ی بعد به فایل BurgerBuilder.js بروید و کد کامنت شده ی خودمان را کات (cut) کنید (همان کدی که در متد purchseContinueHandler قرار داده بودیم). سپس آن را درون OrderHandler قرار دهید:

orderHandler = (event) => {
    event.preventDefault();
    this.setState({ loading: true })
    const order = {
        ingredients: this.state.ingredients,
        price: this.state.totalPrice,
        customer: {
            name: 'Amir Zouerami',
            address: {
                street: 'Teststreet 1',
                zipCode: '9174582541',
                country: 'Iran'
            },
            email: 'test@test.com'
        },
        deliveryMethod: 'fastest'
    }
    axios.post('/orders.json', order)
        .then(response => {
            this.setState({ loading: false, purchasing: false });
        })
        .catch(error => {
            this.setState({ loading: false, purchasing: false });
        });
}

البته این کد کامل نیست و باید تغییراتی در آن بدهیم. اولین تغییری که می دهیم اضافه کردن loading به State همین فایل (ContactData.js) است:

state = {
    name: '',
    email: '',
    address: {
        street: '',
        postalCode: ''
    },
    loading: false
}

این loading برای نمایش spinner (علامت loading) استفاده می شود و باید در حالت اولیه روی False باشد. سپس مقدار ingredients را باید از this.state به this.props تغییر بدهیم:

ingredients: this.state.ingredients,

در خط بعد به Price می رسیم که کارمان را سخت می کند چرا که فعلا فقط درون فایل BurgerBuilder.js محاسبه و نگهداری می شود. بنابراین باید قیمت همبرگر را نیز به Checkout ارسال کنیم. پس وارد فایل BurgerBuilder.js شده و درون متد purchaseContinueHandler مقدار price را نیز به آن می چسبانیم:

purchaseContinueHandler = () => {
    // alert('You continue!');

    const queryParams = [];
    for (let i in this.state.ingredients) {
        queryParams.push(encodeURIComponent(i) + '=' + encodeURIComponent(this.state.ingredients[i]));
    }
    queryParams.push('price=' + this.state.totalPrice);
    const queryString = queryParams.join('&');
    this.props.history.push({
        pathname: '/checkout',
        search: '?' + queryString
    });
}

کد اضافه شده، قسمت زیر است:

queryParams.push('price=' + this.state.totalPrice);

در قدم بعدی به Checkout.js می رویم و تا پارامتر جدید ارسال شده را دریافت کنیم. درون ComponentDidMount یک حلقه ی for داشتیم که ingredient ها را دریافت می کردیم اما قیمت همبرگر جزء ingredients نیست بنابراین نباید درون آرایه ی ingredients قرار بگیرد. ما با استفاده از یک شرط دیگر می گوییم:

componentDidMount() {
    const query = new URLSearchParams(this.props.location.search);
    const ingredients = {};
    let price = 0;
    for (let param of query.entries()) {
        // ['salad', '1']
        if (param[0] === 'price') {
            price = param[1];
        } else {
            ingredients[param[0]] = +param[1];
        }
    }
    this.setState({ ingredients: ingredients, totalPrice: price });
}

بدین شکل گفته ایم اگر در این حلقه به price رسیدیم آن را درون آرایه ی ingredients نگذار بلکه درون متغیری به نام price قرار بده. همچنین در خط آخر برای setState مقدار price را نیز درون state گذاشته ایم. حالا به state در همین فایل (Checkout.js) می رویم و price را نیز به آن اضافه می کنیم:

    state = {
        ingredients: null,
        price: 0
    }

من ingredients را هم حذف کرده و null را برایشان گذاشتم تا به صورت پیش فرض بدون مقدار باشیم. حلقه ای که داریم بعدا مقدار ingredients را درون state می گذارد. در مرحله ی آخر در قسمت JSX بایدprice را نیز به ContactData پاس بدهیم:

<Route
    path={this.props.match.path + '/contact-data'}
    render={(props) => (<ContactData ingredients={this.state.ingredients} price={this.state.totalPrice} />)} />

حالا به ContactData.js و متد orderHandler برمی گردیم و مقدار price را تنظیم می کنیم:

  const order = {
      ingredients: this.props.ingredients,
      price: this.props.price,

قسمت اطلاعات customer را فعلا دست نمی زنیم و در جلسات بعد آن را به فرم خود متصل خواهیم کرد. سپس قسمت purchasing را از setState (درون بلوک axios.post) حذف می کنیم:

axios.post('/orders.json', order)
    .then(response => {
        this.setState({ loading: false });
    })
    .catch(error => {
        this.setState({ loading: false });
    });

مشکل بزرگتر این است که ما یک instance از axios ساخته بودیم ولی آن را در این فایل نداریم. بنابراین آن را در بالای صفحه وارد می کنیم:

import axios from '../../../axios-orders';

در آخرین مرحله باید تغییر کوچکی ایجاد کنیم. باید به فایل Checkout.js بروید و componentDidMount را به componentWillMount تغییر دهید چرا که state در ابتدا null است و این مقدار null به ContactData پاس داده می شود و با null هم که نمی شود همبرگر ساخت! بنابراین به خطا برخورد می کنیم. اگر از componentWillMount استفاده کنیم یعنی قبل از اینکه فرزندان را render کنیم، state را محاسبه می کنیم. در حال حاضر اگر همبرگری را سفارش دهید درون Firebase ثبت خواهد شد.

البته هنوز از قابلیت loading در State استفاده نکرده ایم. برای این کار ابتدا spinner را وارد فایل می کنیم:

import Spinner from '../../../components/UI/Spinner/Spinner';

در مرحله ی بعد کل فرم خود را درون یک متغیر می گذاریم و در صورتی که state.loading صحیح بود، این متغیر را به spinner تغییر می دهیم:

render() {
    let form = (
        <form>
            <input className={classes.Input} type="text" name="name" placeholder="Your Name" />
            <input className={classes.Input} type="email" name="email" placeholder="Your Mail" />
            <input className={classes.Input} type="text" name="street" placeholder="Street" />
            <input className={classes.Input} type="text" name="postal" placeholder="Postal Code" />
            <Button btnType="Success" clicked={this.orderHandler}>ORDER</Button>
        </form>
    );
    if (this.state.loading) {
        form = <Spinner />;
    }
    return (
        <div className={classes.ContactData}>
            <h4>Enter your Contact Data</h4>
            {form}
        </div>
    );
}

به همین راحتی کارمان را انجام داده ایم.

در مرحله ی آخر می خواهیم کاربر پس از ثبت سفارش redirect شود اما نمی توانیم به راحتی بگوییم:

this.props.history.push('/');

آیا می دانید چرا؟ به دلیل اینکه این کامپوننت (ContactData) را به شکل زیر بارگذاری کرده ایم:

<Route
    path={this.props.match.path + '/contact-data'}
    render={() => (<ContactData ingredients={this.state.ingredients} price={this.state.totalPrice}/>)} />

از آنجایی که به جای component از render استفاده کرده ایم دیگر به شیء history دسترسی نداریم تا با آن صفحه ی جدیدی را push کنیم. برای حل این مشکل دو راه وجود دارد:

راه اول این است که از withRouter برای export کردن ContactData استفاده کنیم. راه دوم نیز این است که prop های checkout را به ContactData پاس بدهیم.

من روش دوم را انتخاب می کنم:

<Route
    path={this.props.match.path + '/contact-data'}
    render={(props) => (<ContactData ingredients={this.state.ingredients} price={this.state.totalPrice} {...props} />)} />

حالا props را دریافت کرده ام و آن را با اپراتور Spread به ContactData پاس داده ام. بنابراین در فایل ContactData و در قسمت post در axios می گوییم:

axios.post('/orders.json', order)
    .then(response => {
        this.setState({ loading: false });
        this.props.history.push('/');
    })
    .catch(error => {
        this.setState({ loading: false });
    });

اکنون اگر کاربر سفارشی را ثبت کند، پس از کلیک روی ORDER به صفحه ی اصلی (/) برمی گردد. می توانید خودتان این موضوع را در مرورگر تست کنید. در قسمت بعد روی صفحه ی my orders کار خواهیم کرد.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری دوره جامع آموزش ری اکت توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما (1 دیدگاه)

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

رحیمی
26 آبان 1400
سلام انتقال داده ها از طریق state از امنیت کافی برخوردار هست؟ برای انتقال داده های حساس و مهم بین کامپوننت ها از چه روشی استفاده کنیم که کاربر نتواند آن را دستکاری کند؟؟

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.